Repository: huggingface/trl Branch: main Commit: 8e6e0626ebec Files: 380 Total size: 6.3 MB Directory structure: gitextract_r678upi2/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yml │ │ ├── feature-request.yml │ │ └── new-trainer-addition.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── codeql/ │ │ └── custom-queries.qls │ └── workflows/ │ ├── build_documentation.yml │ ├── build_pr_documentation.yml │ ├── clear_cache.yml │ ├── codeQL.yml │ ├── docker-build.yml │ ├── issue_auto_labeller.yml │ ├── pr_style_bot.yml │ ├── publish.yml │ ├── slow-tests.yml │ ├── tests-experimental.yml │ ├── tests.yml │ ├── tests_latest.yml │ ├── tests_transformers_branch.yml │ ├── trufflehog.yml │ └── upload_pr_documentation.yml ├── .gitignore ├── .pre-commit-config.yaml ├── AGENTS.md ├── CITATION.cff ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── MIGRATION.md ├── Makefile ├── README.md ├── RELEASE.md ├── VERSION ├── docker/ │ ├── trl/ │ │ └── Dockerfile │ └── trl-dev/ │ └── Dockerfile ├── docs/ │ └── source/ │ ├── _toctree.yml │ ├── async_grpo_trainer.md │ ├── bco_trainer.md │ ├── bema_for_reference_model.md │ ├── callbacks.md │ ├── chat_template_utils.md │ ├── clis.md │ ├── community_tutorials.md │ ├── cpo_trainer.md │ ├── customization.md │ ├── data_utils.md │ ├── dataset_formats.md │ ├── deepspeed_integration.md │ ├── distributing_training.md │ ├── dpo_trainer.md │ ├── example_overview.md │ ├── experimental_overview.md │ ├── gfpo.md │ ├── gkd_trainer.md │ ├── gold_trainer.md │ ├── grpo_trainer.md │ ├── grpo_with_replay_buffer.md │ ├── gspo_token.md │ ├── index.md │ ├── installation.md │ ├── jobs_training.md │ ├── judges.md │ ├── kernels_hub.md │ ├── kto_trainer.md │ ├── liger_kernel_integration.md │ ├── lora_without_regret.md │ ├── merge_model_callback.md │ ├── minillm_trainer.md │ ├── nash_md_trainer.md │ ├── nemo_gym.md │ ├── online_dpo_trainer.md │ ├── openenv.md │ ├── orpo_trainer.md │ ├── paper_index.md │ ├── papo_trainer.md │ ├── peft_integration.md │ ├── ppo_trainer.md │ ├── prm_trainer.md │ ├── ptt_integration.md │ ├── quickstart.md │ ├── rapidfire_integration.md │ ├── reducing_memory_usage.md │ ├── reward_trainer.md │ ├── rewards.md │ ├── rloo_trainer.md │ ├── script_utils.md │ ├── sft_trainer.md │ ├── speeding_up_training.md │ ├── trackio_integration.md │ ├── unsloth_integration.md │ ├── use_model.md │ ├── vllm_integration.md │ ├── winrate_callback.md │ └── xpo_trainer.md ├── examples/ │ ├── README.md │ ├── accelerate_configs/ │ │ ├── alst_ulysses_4gpu.yaml │ │ ├── context_parallel_2gpu.yaml │ │ ├── deepspeed_zero1.yaml │ │ ├── deepspeed_zero2.yaml │ │ ├── deepspeed_zero3.yaml │ │ ├── fsdp1.yaml │ │ ├── fsdp2.yaml │ │ ├── multi_gpu.yaml │ │ └── single_gpu.yaml │ ├── cli_configs/ │ │ └── example_config.yaml │ ├── datasets/ │ │ ├── deepmath_103k.py │ │ ├── hh-rlhf-helpful-base.py │ │ ├── llava_instruct_mix.py │ │ ├── lm-human-preferences-descriptiveness.py │ │ ├── lm-human-preferences-sentiment.py │ │ ├── math_shepherd.py │ │ ├── prm800k.py │ │ ├── rlaif-v.py │ │ ├── tldr.py │ │ ├── tldr_preference.py │ │ ├── ultrafeedback-prompt.py │ │ └── ultrafeedback.py │ ├── notebooks/ │ │ ├── README.md │ │ ├── grpo_agent.ipynb │ │ ├── grpo_functiongemma_browsergym_openenv.ipynb │ │ ├── grpo_ministral3_vl.ipynb │ │ ├── grpo_qwen3_vl.ipynb │ │ ├── grpo_rnj_1_instruct.ipynb │ │ ├── grpo_trl_lora_qlora.ipynb │ │ ├── openenv_sudoku_grpo.ipynb │ │ ├── openenv_wordle_grpo.ipynb │ │ ├── sft_ministral3_vl.ipynb │ │ ├── sft_nemotron_3.ipynb │ │ ├── sft_qwen_vl.ipynb │ │ ├── sft_tool_calling.ipynb │ │ └── sft_trl_lora_qlora.ipynb │ └── scripts/ │ ├── async_grpo.py │ ├── bco.py │ ├── cpo.py │ ├── dpo.py │ ├── dpo_vlm.py │ ├── evals/ │ │ └── judge_tldr.py │ ├── gkd.py │ ├── grpo_2048.py │ ├── grpo_agent.py │ ├── grpo_vlm.py │ ├── gspo.py │ ├── gspo_vlm.py │ ├── kto.py │ ├── mpo_vlm.py │ ├── nash_md.py │ ├── nemo_gym/ │ │ ├── README.md │ │ ├── config.yaml │ │ ├── deepspeed_zero3.yaml │ │ ├── submit.sh │ │ └── train_multi_environment.py │ ├── online_dpo.py │ ├── online_dpo_vlm.py │ ├── openenv/ │ │ ├── browsergym.py │ │ ├── browsergym_llm.py │ │ ├── carla.py │ │ ├── catch.py │ │ ├── echo.py │ │ ├── sudoku.py │ │ ├── sudoku_prompt.txt │ │ └── wordle.py │ ├── orpo.py │ ├── ppo/ │ │ ├── ppo.py │ │ └── ppo_tldr.py │ ├── prm.py │ ├── reward_modeling.py │ ├── rloo.py │ ├── rloo_vlm.py │ ├── sft.py │ ├── sft_gemma3.py │ ├── sft_gpt_oss.py │ ├── sft_nemotron_3.py │ ├── sft_tiny_aya_tool_calling.py │ ├── sft_video_llm.py │ ├── sft_vlm.py │ ├── sft_vlm_gemma3.py │ ├── tiny_aya_chat_template.jinja │ └── xpo.py ├── pyproject.toml ├── requirements.txt ├── scripts/ │ ├── add_copyrights.py │ ├── generate_harmony_dataset.py │ ├── generate_tiny_models.py │ ├── generate_toolcall_dataset.py │ ├── generate_zen_dataset.py │ ├── generate_zen_image_dataset.py │ ├── generate_zen_multi_image_dataset.py │ └── log_reports.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ ├── data/ │ │ └── template.jinja │ ├── distributed/ │ │ ├── __init__.py │ │ ├── data/ │ │ │ └── accelerate_configs/ │ │ │ ├── ddp.yaml │ │ │ ├── fsdp2.yaml │ │ │ ├── zero2.yaml │ │ │ └── zero3.yaml │ │ └── test_distributed.py │ ├── experimental/ │ │ ├── __init__.py │ │ ├── test_async_grpo_trainer.py │ │ ├── test_bco_trainer.py │ │ ├── test_cpo_trainer.py │ │ ├── test_dppo_trainer.py │ │ ├── test_gkd_trainer.py │ │ ├── test_gold_trainer.py │ │ ├── test_grpo_with_replay_buffer_trainer.py │ │ ├── test_gspo_token_trainer.py │ │ ├── test_judges.py │ │ ├── test_kto_trainer.py │ │ ├── test_merge_model_callback.py │ │ ├── test_minillm_trainer.py │ │ ├── test_modeling_value_head.py │ │ ├── test_nash_md_trainer.py │ │ ├── test_online_dpo_trainer.py │ │ ├── test_orpo_trainer.py │ │ ├── test_ppo_trainer.py │ │ ├── test_prm_trainer.py │ │ ├── test_utils.py │ │ ├── test_winrate_callback.py │ │ ├── test_xpo_trainer.py │ │ └── testing_utils.py │ ├── test_activation_offloading.py │ ├── test_callbacks.py │ ├── test_chat_template_utils.py │ ├── test_cli.py │ ├── test_cli_utils.py │ ├── test_data_utils.py │ ├── test_dpo_trainer.py │ ├── test_grpo_trainer.py │ ├── test_model_utils.py │ ├── test_reward_trainer.py │ ├── test_rewards.py │ ├── test_rich_progress_callback.py │ ├── test_rloo_trainer.py │ ├── test_sft_trainer.py │ ├── test_skills.py │ ├── test_skills_cli.py │ ├── test_utils.py │ ├── test_vllm_client_server.py │ ├── testing_constants.py │ └── testing_utils.py └── trl/ ├── __init__.py ├── _compat.py ├── _lazy_module.py ├── accelerate_configs/ │ ├── fsdp1.yaml │ ├── fsdp2.yaml │ ├── multi_gpu.yaml │ ├── single_gpu.yaml │ ├── zero1.yaml │ ├── zero2.yaml │ └── zero3.yaml ├── chat_template_utils.py ├── cli/ │ ├── __init__.py │ ├── accelerate_config.py │ ├── accelerate_launcher.py │ ├── commands/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── env.py │ │ ├── skills.py │ │ ├── training.py │ │ └── vllm_serve.py │ └── main.py ├── data_utils.py ├── experimental/ │ ├── __init__.py │ ├── async_grpo/ │ │ ├── __init__.py │ │ ├── async_grpo_config.py │ │ ├── async_grpo_trainer.py │ │ └── async_rollout_worker.py │ ├── bco/ │ │ ├── __init__.py │ │ ├── bco_config.py │ │ └── bco_trainer.py │ ├── bema_for_ref_model/ │ │ ├── __init__.py │ │ ├── callback.py │ │ └── dpo_trainer.py │ ├── cpo/ │ │ ├── __init__.py │ │ ├── cpo_config.py │ │ └── cpo_trainer.py │ ├── dppo/ │ │ ├── __init__.py │ │ ├── dppo_config.py │ │ └── dppo_trainer.py │ ├── gfpo/ │ │ ├── __init__.py │ │ ├── gfpo_config.py │ │ └── gfpo_trainer.py │ ├── gkd/ │ │ ├── __init__.py │ │ ├── gkd_config.py │ │ └── gkd_trainer.py │ ├── gold/ │ │ ├── __init__.py │ │ ├── gold.py │ │ ├── gold_config.py │ │ └── gold_trainer.py │ ├── grpo_with_replay_buffer/ │ │ ├── __init__.py │ │ ├── grpo_with_replay_buffer_config.py │ │ └── grpo_with_replay_buffer_trainer.py │ ├── gspo_token/ │ │ ├── __init__.py │ │ └── grpo_trainer.py │ ├── judges/ │ │ ├── __init__.py │ │ └── judges.py │ ├── kto/ │ │ ├── __init__.py │ │ ├── kto_config.py │ │ └── kto_trainer.py │ ├── merge_model_callback.py │ ├── minillm/ │ │ ├── __init__.py │ │ ├── minillm_config.py │ │ └── minillm_trainer.py │ ├── nash_md/ │ │ ├── __init__.py │ │ ├── nash_md_config.py │ │ └── nash_md_trainer.py │ ├── online_dpo/ │ │ ├── __init__.py │ │ ├── online_dpo_config.py │ │ └── online_dpo_trainer.py │ ├── openenv/ │ │ ├── __init__.py │ │ └── utils.py │ ├── orpo/ │ │ ├── __init__.py │ │ ├── orpo_config.py │ │ └── orpo_trainer.py │ ├── papo/ │ │ ├── __init__.py │ │ ├── papo_config.py │ │ └── papo_trainer.py │ ├── ppo/ │ │ ├── __init__.py │ │ ├── modeling_value_head.py │ │ ├── ppo_config.py │ │ └── ppo_trainer.py │ ├── prm/ │ │ ├── __init__.py │ │ ├── prm_config.py │ │ └── prm_trainer.py │ ├── utils.py │ ├── winrate_callback.py │ └── xpo/ │ ├── __init__.py │ ├── xpo_config.py │ └── xpo_trainer.py ├── extras/ │ ├── __init__.py │ ├── dataset_formatting.py │ └── profiling.py ├── generation/ │ ├── __init__.py │ ├── vllm_client.py │ └── vllm_generation.py ├── import_utils.py ├── models/ │ ├── __init__.py │ ├── activation_offloading.py │ └── utils.py ├── py.typed ├── rewards/ │ ├── __init__.py │ ├── accuracy_rewards.py │ ├── format_rewards.py │ └── other_rewards.py ├── scripts/ │ ├── __init__.py │ ├── _hf_argparser.py │ ├── dpo.py │ ├── env.py │ ├── grpo.py │ ├── kto.py │ ├── reward.py │ ├── rloo.py │ ├── sft.py │ ├── utils.py │ └── vllm_serve.py ├── skills/ │ ├── __init__.py │ ├── cli.py │ ├── skills.py │ └── trl-training/ │ └── SKILL.md ├── templates/ │ ├── completions_dataset_card.md │ ├── lm_model_card.md │ └── rm_model_card.md └── trainer/ ├── __init__.py ├── base_config.py ├── base_trainer.py ├── callbacks.py ├── dpo_config.py ├── dpo_trainer.py ├── grpo_config.py ├── grpo_trainer.py ├── kto_config.py ├── kto_trainer.py ├── model_config.py ├── reward_config.py ├── reward_trainer.py ├── rloo_config.py ├── rloo_trainer.py ├── sft_config.py ├── sft_trainer.py └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yml ================================================ name: "\U0001F41B Bug Report" description: Submit a bug report to help us improve TRL labels: [ "bug" ] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! 🤗 🚩 If it is your first time submitting, be sure to check our [bug report guidelines](https://github.com/huggingface/trl/blob/main/CONTRIBUTING.md#did-you-find-a-bug) - type: textarea id: reproduction validations: required: true attributes: label: Reproduction description: | Please provide a code sample that reproduces the problem you ran into. It can be a Colab link or just a code snippet. If you have code snippets, error messages, stack traces please provide them here as well. Important! Use code tags to correctly format your code. See https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks#syntax-highlighting Do not use screenshots, as they are hard to read and (more importantly) don't allow others to copy-and-paste your code. value: | ```python from trl import ... ``` outputs: ``` Traceback (most recent call last): File "example.py", line 42, in ... ``` - type: textarea id: system-info attributes: label: System Info description: | Please provide information about your system: platform, Python version, PyTorch version, Transformers version, devices, TRL version, ... You can get this information by running `trl env` in your terminal. placeholder: Copy-paste the output of `trl env` validations: required: true - type: checkboxes id: terms attributes: label: Checklist description: | Before submitting, please confirm that you've completed each of the following. If an item doesn't apply to your issue, check it anyway to show you've reviewed it. options: - label: "I have checked that my issue isn't already filed (see [open issues](https://github.com/huggingface/trl/issues?q=is%3Aissue))" required: true - label: "I have included my system information" required: true - label: "Any code provided is minimal, complete, and reproducible ([more on MREs](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))" required: true - label: "Any code provided is properly formatted in code blocks, (no screenshot, [more on code blocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks))" required: true - label: "Any traceback provided is complete" required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yml ================================================ name: "\U0001F680 Feature request" description: Submit a proposal/request for a new TRL feature labels: [ "Feature request" ] body: - type: textarea id: feature-request validations: required: true attributes: label: Feature request description: | A clear and concise description of the feature proposal. Please provide a link to the paper and code in case they exist. - type: textarea id: motivation validations: required: true attributes: label: Motivation description: | Please outline the motivation for the proposal. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another GitHub issue, please link here too. - type: textarea id: contribution validations: required: true attributes: label: Your contribution description: | Is there any way that you could help, e.g. by submitting a PR? Make sure to read the CONTRIBUTING.MD [readme](https://github.com/huggingface/trl/blob/main/CONTRIBUTING.md) ================================================ FILE: .github/ISSUE_TEMPLATE/new-trainer-addition.yml ================================================ name: "\U0001F31F New trainer addition" description: Submit a proposal/request to implement a new trainer for a post-training method labels: [ "New trainer" ] body: - type: textarea id: description-request validations: required: true attributes: label: Method description description: | Put any and all important information relative to the method - type: checkboxes id: information-tasks attributes: label: Open source status description: | Please note that if the method implementation isn't available or model weights with training datasets aren't available, we are less likely to implement it in `trl`. options: - label: "The method implementation is available" - label: "The model weights are available" - label: "The training datasets are available" - type: textarea id: additional-info attributes: label: Provide useful links for the implementation description: | Please provide information regarding the implementation, the weights, and the authors. Please mention the authors by @gh-username if you're aware of their usernames. ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # What does this PR do? Fixes # (issue) ## Before submitting - [ ] This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case). - [ ] Did you read the [contributor guideline](https://github.com/huggingface/trl/blob/main/CONTRIBUTING.md#create-a-pull-request), Pull Request section? - [ ] Was this discussed/approved via a GitHub issue? Please add a link to it if that's the case. - [ ] Did you make sure to update the documentation with your changes? - [ ] Did you write any new necessary tests? ## Who can review? Anyone in the community is free to review the PR once the tests have passed. Feel free to tag members/contributors who may be interested in your PR. ================================================ FILE: .github/codeql/custom-queries.qls ================================================ import codeql from WorkflowString interpolation, Workflow workflow where interpolation.getStringValue().matches("${{ github.event.issue.title }}") or interpolation.getStringValue().matches("${{ github.event.issue.body }}") or interpolation.getStringValue().matches("${{ github.event.pull_request.title }}") or interpolation.getStringValue().matches("${{ github.event.pull_request.body }}") or interpolation.getStringValue().matches("${{ github.event.review.body }}") or interpolation.getStringValue().matches("${{ github.event.comment.body }}") or interpolation.getStringValue().matches("${{ github.event.inputs.* }}") or interpolation.getStringValue().matches("${{ github.event.head_commit.message }}") interpolation.getStringValue().matches("${{ github.event.* }}") and ( step.getKey() = "run" or // Injection in run step.getKey() = "env" or // Injection via env step.getKey() = "with" // Injection via with ) select workflow, "🚨 Do not use directly as input of action" ================================================ FILE: .github/workflows/build_documentation.yml ================================================ name: Build documentation on: push: branches: - main - doc-builder* - v*-release env: TRL_EXPERIMENTAL_SILENCE: 1 jobs: build: uses: huggingface/doc-builder/.github/workflows/build_main_documentation.yml@main with: commit_sha: ${{ github.sha }} package: trl version_tag_suffix: "" secrets: hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} ================================================ FILE: .github/workflows/build_pr_documentation.yml ================================================ name: Build PR Documentation on: pull_request: env: TRL_EXPERIMENTAL_SILENCE: 1 concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: build: if: github.event.pull_request.draft == false uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main with: commit_sha: ${{ github.event.pull_request.head.sha }} pr_number: ${{ github.event.number }} package: trl version_tag_suffix: "" ================================================ FILE: .github/workflows/clear_cache.yml ================================================ name: "Cleanup Cache" on: workflow_dispatch: schedule: - cron: "0 0 * * *" jobs: cleanup: runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v6 - name: Cleanup run: | gh extension install actions/gh-actions-cache REPO=${{ github.repository }} echo "Fetching list of cache key" cacheKeysForPR=$(gh actions-cache list -R $REPO | cut -f 1 ) ## Setting this to not fail the workflow while deleting cache keys. set +e echo "Deleting caches..." for cacheKey in $cacheKeysForPR do gh actions-cache delete $cacheKey -R $REPO --confirm done echo "Done" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/codeQL.yml ================================================ name: "CodeQL Analysis - Workflows" on: workflow_dispatch: jobs: analyze: name: "Analyze GitHub Workflows" runs-on: ubuntu-latest permissions: security-events: write actions: read contents: read steps: - name: "Checkout repository" uses: actions/checkout@v6 - name: "Initialize CodeQL" uses: github/codeql-action/init@v2 with: languages: "yaml" queries: +security-and-quality, ./.github/codeql/custom-queries.qls - name: "Perform CodeQL Analysis" uses: github/codeql-action/analyze@v2 ================================================ FILE: .github/workflows/docker-build.yml ================================================ name: Build TRL Docker image on: push: branches: - main workflow_dispatch: concurrency: group: docker-image-builds cancel-in-progress: false jobs: trl: name: "Build and push TRL Docker image" runs-on: group: aws-general-8-plus steps: - name: Checkout code uses: actions/checkout@v6 - name: Get TRL version from PyPI run: | VERSION=$(curl -s https://pypi.org/pypi/trl/json | jq -r .info.version) echo "VERSION=$VERSION" >> $GITHUB_ENV - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and Push uses: docker/build-push-action@v6 with: context: docker/trl push: true tags: | huggingface/trl:${{ env.VERSION }} huggingface/trl - name: Post to Slack if: always() uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ secrets.CI_DOCKER_CHANNEL }} title: 🤗 Results of the TRL Dev Docker Image build status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} trl-dev: name: "Build and push TRL Dev Docker image" runs-on: group: aws-general-8-plus steps: - name: Checkout code uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Build and Push uses: docker/build-push-action@v6 with: context: docker/trl-dev push: true tags: | huggingface/trl:dev - name: Post to Slack if: always() uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ secrets.CI_DOCKER_CHANNEL }} title: 🤗 Results of the TRL Dev Docker Image build status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} ================================================ FILE: .github/workflows/issue_auto_labeller.yml ================================================ name: "Hugging Face Issue Labeler" on: issues: types: opened jobs: triage: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@v6 - uses: August-murr/auto-labeler@0.0.1 with: hf-api-key: ${{ secrets.CI_HF_API_TOKEN }} ================================================ FILE: .github/workflows/pr_style_bot.yml ================================================ name: PR Style Bot on: workflow_dispatch: permissions: contents: write pull-requests: write jobs: run-style-bot: if: > contains(github.event.comment.body, '@bot /style') && github.event.issue.pull_request != null runs-on: ubuntu-latest steps: - name: Extract PR details id: pr_info uses: actions/github-script@v8 with: script: | const prNumber = context.payload.issue.number; const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); // We capture both the branch ref and the "full_name" of the head repo // so that we can check out the correct repository & branch (including forks). core.setOutput("prNumber", prNumber); core.setOutput("headRef", pr.head.ref); core.setOutput("headRepoFullName", pr.head.repo.full_name); - name: Check out PR branch uses: actions/checkout@v6 env: HEADREPOFULLNAME: ${{ steps.pr_info.outputs.headRepoFullName }} HEADREF: ${{ steps.pr_info.outputs.headRef }} with: # Instead of checking out the base repo, use the contributor's repo name repository: ${{ env.HEADREPOFULLNAME }} ref: ${{ env.HEADREF }} # You may need fetch-depth: 0 for being able to push fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Debug env: HEADREPOFULLNAME: ${{ steps.pr_info.outputs.headRepoFullName }} HEADREF: ${{ steps.pr_info.outputs.headRef }} PRNUMBER: ${{ steps.pr_info.outputs.prNumber }} run: | echo "PR number: ${{ env.PRNUMBER }}" echo "Head Ref: ${{ env.HEADREF }}" echo "Head Repo Full Name: ${{ env.HEADREPOFULLNAME }}" - name: Set up Python uses: actions/setup-python@v6 - name: Install dependencies run: | pip install ruff pre-commit - name: Download Makefile from main branch run: | curl -o main_Makefile https://raw.githubusercontent.com/huggingface/trl/main/Makefile - name: Compare Makefiles run: | if ! diff -q main_Makefile Makefile; then echo "Error: The Makefile has changed. Please ensure it matches the main branch." exit 1 fi echo "No changes in Makefile. Proceeding..." rm -rf main_Makefile - name: Run make style and make quality run: | make precommit || true - name: Commit and push changes id: commit_and_push env: HEADREPOFULLNAME: ${{ steps.pr_info.outputs.headRepoFullName }} HEADREF: ${{ steps.pr_info.outputs.headRef }} PRNUMBER: ${{ steps.pr_info.outputs.prNumber }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "HEADREPOFULLNAME: ${{ env.HEADREPOFULLNAME }}, HEADREF: ${{ env.HEADREF }}" # Configure git with the Actions bot user git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" # Make sure your 'origin' remote is set to the contributor's fork git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ env.HEADREPOFULLNAME }}.git" # If there are changes after running style/quality, commit them if [ -n "$(git status --porcelain)" ]; then git add . git commit -m "Apply style fixes" # Push to the original contributor's forked branch git push origin HEAD:${{ env.HEADREF }} echo "changes_pushed=true" >> $GITHUB_OUTPUT else echo "No changes to commit." echo "changes_pushed=false" >> $GITHUB_OUTPUT fi - name: Comment on PR with workflow run link if: steps.commit_and_push.outputs.changes_pushed == 'true' uses: actions/github-script@v8 with: script: | const prNumber = parseInt(process.env.prNumber, 10); const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: `Style fixes have been applied. [View the workflow run here](${runUrl}).` }); env: prNumber: ${{ steps.pr_info.outputs.prNumber }} ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to PyPI on: push: branches: - main - v*-release paths: - "VERSION" jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Read version id: get_version run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT - name: Debug - Show version.txt content run: echo "Version is ${{ steps.get_version.outputs.version }}" - name: Set up Python uses: actions/setup-python@v6 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install build twine - name: Build package run: python -m build - name: Publish to PyPI if: ${{ !contains(steps.get_version.outputs.version, 'dev') }} env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | python -m twine upload dist/* ================================================ FILE: .github/workflows/slow-tests.yml ================================================ name: Slow tests (on push) on: push: branches: [main] paths: # Run only when python files are modified - "trl/**.py" - "examples/**.py" env: RUN_SLOW: "yes" IS_GITHUB_CI: "1" SLACK_API_TOKEN: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} TRL_EXPERIMENTAL_SILENCE: 1 jobs: run_all_tests_single_gpu: runs-on: group: aws-g4dn-2xlarge env: CUDA_VISIBLE_DEVICES: "0" TEST_TYPE: "single_gpu" container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all --shm-size "16gb" defaults: run: shell: bash steps: - name: Git checkout uses: actions/checkout@v6 - name: Install system dependencies run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install pytest-reportlog - name: Run slow SFT tests on single GPU if: always() run: | source .venv/bin/activate make slow_tests - name: Generate Report if: always() run: | source .venv/bin/activate uv pip install slack_sdk tabulate python scripts/log_reports.py >> $GITHUB_STEP_SUMMARY run_all_tests_multi_gpu: runs-on: group: aws-g4dn-2xlarge env: CUDA_VISIBLE_DEVICES: "0,1" TEST_TYPE: "multi_gpu" container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all --shm-size "16gb" defaults: run: shell: bash steps: - name: Git checkout uses: actions/checkout@v6 - name: Install system dependencies run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install pytest-reportlog - name: Run slow SFT tests on Multi GPU if: always() run: | source .venv/bin/activate make slow_tests - name: Generate Reports if: always() run: | source .venv/bin/activate uv pip install slack_sdk tabulate python scripts/log_reports.py >> $GITHUB_STEP_SUMMARY rm *.txt ================================================ FILE: .github/workflows/tests-experimental.yml ================================================ name: Tests (experimental) on: pull_request: paths: # Run only when relevant files are modified - "trl/experimental/**" - "tests/experimental/**" env: TQDM_DISABLE: 1 PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:True" PYTORCH_ALLOC_CONF: "expandable_segments:True" TRL_EXPERIMENTAL_SILENCE: 1 jobs: check_code_quality: name: Check code quality runs-on: ubuntu-latest if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v6 - name: Set up Python 3.13 uses: actions/setup-python@v6 with: python-version: 3.13 - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files tests: name: Tests (experimental) runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.13 uses: actions/setup-python@v6 with: python-version: 3.13 - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" - name: Test with pytest run: | source .venv/bin/activate make test_experimental ================================================ FILE: .github/workflows/tests.yml ================================================ name: Tests on: push: branches: - main - ci-* pull_request: paths: # Run only when relevant files are modified - ".github/**.yml" - "examples/**.py" - "scripts/**.py" - "tests/**.py" - "trl/**.py" - "pyproject.toml" # Exclude if only experimental code/tests - "!trl/experimental/**" - "!tests/experimental/**" env: TQDM_DISABLE: 1 CI_SLACK_CHANNEL: ${{ secrets.CI_PUSH_MAIN_CHANNEL }} PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:True" PYTORCH_ALLOC_CONF: "expandable_segments:True" jobs: check_code_quality: name: Check code quality runs-on: ubuntu-latest if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: 3.12 - uses: pre-commit/action@v3.0.1 with: extra_args: --all-files tests: name: Tests strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] fail-fast: false runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash if: github.event.pull_request.draft == false steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack if: github.ref == 'refs/heads/main' && always() # Check if the branch is main uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results with Python ${{ matrix.python-version }} and latest dependencies status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} tests_dev: name: Tests with dev dependencies runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash if: github.event.pull_request.draft == false steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install -U git+https://github.com/huggingface/accelerate.git uv pip install -U git+https://github.com/huggingface/datasets.git uv pip install -U git+https://github.com/huggingface/transformers.git uv pip install -U git+https://github.com/huggingface/peft.git - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack if: github.ref == 'refs/heads/main' && always() # Check if the branch is main uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results with Python 3.12 and dev dependencies status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} tests_wo_optional_deps: name: Tests without optional dependencies runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash if: github.event.pull_request.draft == false steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[test]" - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack if: github.ref == 'refs/heads/main' && always() # Check if the branch is main uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results with Python 3.12 without optional dependencies status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} tests_min_versions: name: Tests with minimum versions runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash if: github.event.pull_request.draft == false steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install accelerate==1.4.0 uv pip install datasets==3.0.0 uv pip install transformers==4.56.2 - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack if: github.ref == 'refs/heads/main' && always() # Check if the branch is main uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results with Python 3.12 and minimum dependencies versions status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} distributed_smoke: name: Distributed smoke tests runs-on: group: aws-g5-12xlarge-cache container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash if: github.event.pull_request.draft == false env: CUDA_VISIBLE_DEVICES: "0,1" steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" - name: Run distributed smoke tests run: | source .venv/bin/activate pytest -v tests/distributed/test_distributed.py - name: Post to Slack if: github.ref == 'refs/heads/main' && always() # Check if the branch is main uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results of distributed smoke tests status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} ================================================ FILE: .github/workflows/tests_latest.yml ================================================ name: Tests latest TRL release with dev dependencies on: schedule: - cron: '0 0 * * *' # Runs daily at midnight UTC workflow_dispatch: env: TQDM_DISABLE: 1 CI_SLACK_CHANNEL: ${{ secrets.CI_PUSH_MAIN_CHANNEL }} TRL_EXPERIMENTAL_SILENCE: 1 jobs: tests: name: Tests latest TRL release with dev dependencies runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash steps: - name: Git checkout uses: actions/checkout@v6 with: { ref: v0.29-release } - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install -U git+https://github.com/huggingface/accelerate.git uv pip install -U git+https://github.com/huggingface/datasets.git uv pip install -U git+https://github.com/huggingface/transformers.git - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results of latest TRL with Python 3.12 and dev dependencies status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} ================================================ FILE: .github/workflows/tests_transformers_branch.yml ================================================ name: Tests against Transformers branch on: workflow_dispatch: inputs: transformers_ref: description: "Transformers git ref (branch, tag, or commit SHA)" required: true default: "main" env: TQDM_DISABLE: 1 CI_SLACK_CHANNEL: ${{ secrets.CI_PUSH_MAIN_CHANNEL }} PYTORCH_CUDA_ALLOC_CONF: "expandable_segments:True" PYTORCH_ALLOC_CONF: "expandable_segments:True" jobs: tests_transformers_branch: name: Tests with Transformers ${{ inputs.transformers_ref }} runs-on: group: aws-g4dn-2xlarge container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install -U git+https://github.com/huggingface/transformers.git@${{ inputs.transformers_ref }} - name: Test with pytest run: | source .venv/bin/activate make test - name: Post to Slack if: github.ref == 'refs/heads/main' && always() uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results with Transformers ${{ inputs.transformers_ref }} status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} distributed_smoke: name: Distributed smoke tests with Transformers ${{ inputs.transformers_ref }} runs-on: group: aws-g5-12xlarge-cache container: image: pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel options: --gpus all defaults: run: shell: bash env: CUDA_VISIBLE_DEVICES: "0,1" steps: - name: Git checkout uses: actions/checkout@v6 - name: Set up Python 3.12 uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install Make and Git run: | apt-get update && apt-get install -y make git curl - name: Install uv run: | curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create Python virtual environment run: | uv venv uv pip install --upgrade setuptools wheel - name: Install dependencies run: | source .venv/bin/activate uv pip install ".[dev]" uv pip install -U git+https://github.com/huggingface/transformers.git@${{ inputs.transformers_ref }} - name: Run distributed smoke tests run: | source .venv/bin/activate pytest -v tests/distributed/test_distributed.py - name: Post to Slack if: github.ref == 'refs/heads/main' && always() uses: huggingface/hf-workflows/.github/actions/post-slack@main with: slack_channel: ${{ env.CI_SLACK_CHANNEL }} title: Results of distributed smoke tests with Transformers ${{ inputs.transformers_ref }} status: ${{ job.status }} slack_token: ${{ secrets.SLACK_CIFEEDBACK_BOT_TOKEN }} ================================================ FILE: .github/workflows/trufflehog.yml ================================================ on: push: name: Secret Leaks jobs: trufflehog: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Secret Scanning uses: trufflesecurity/trufflehog@v3.93.1 with: # exclude buggy postgres detector that is causing false positives and not relevant to our codebase extra_args: --results=verified,unknown --exclude-detectors=postgres ================================================ FILE: .github/workflows/upload_pr_documentation.yml ================================================ name: Upload PR Documentation on: workflow_run: workflows: ["Build PR Documentation"] types: - completed jobs: build: uses: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml@main with: package_name: trl secrets: hf_token: ${{ secrets.HF_DOC_BUILD_PUSH }} comment_bot_token: ${{ secrets.COMMENT_BOT_TOKEN }} ================================================ FILE: .gitignore ================================================ *.bak .gitattributes .last_checked .gitconfig *.bak *.log *~ ~* _tmp* tmp* tags # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .vscode *.swp # osx generated files .DS_Store .DS_Store? .Trashes ehthumbs.db Thumbs.db .idea # pytest .pytest_cache # tools/trust-doc-nbs docs_src/.last_checked # symlinks to fastai docs_src/fastai tools/fastai # link checker checklink/cookies.txt # .gitconfig is now autogenerated .gitconfig # wandb files nbs/wandb/ examples/notebooks/wandb/ wandb/ # uv uv.lock ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.13.3 hooks: - id: ruff-check types_or: [ python, pyi ] args: [ --fix ] - id: ruff-format types_or: [ python, pyi ] # - repo: https://github.com/codespell-project/codespell # rev: v2.1.0 # hooks: # - id: codespell # args: # - --ignore-words-list=nd,reacher,thist,ths,magent,ba # - --skip=docs/css/termynal.css,docs/js/termynal.js ================================================ FILE: AGENTS.md ================================================ # AGENTS.md ## Repository-specific guidance ### Main code vs experimental code The repository is separated into **main code** and **experimental code**. * **Main code** should remain stable, consistent, and well-tested. * **Experimental code** may be less stable and may contain inconsistent patterns or limited testing. Small non-invasive improvements that make experimental code more consistent with the main codebase are encouraged, but avoid large refactors. ### Paper implementations If a PR implements a method, algorithm, or training approach from a research paper, it must also add a corresponding subsection to `paper_index.md`. When reviewing such PRs, ensure that `paper_index.md` was updated. ### Code duplication and consistency Trainers in this repository are **self-contained by design**. Shared logic (generation, reward computation, metric logging, weight syncing, etc.) is deliberately duplicated across trainers rather than abstracted into a shared base class. This is intentional: each trainer must be readable, modifiable, and evolvable in isolation. The base class (`_BaseTrainer`) provides only minimal utilities (model card generation). Everything else — vLLM generation paths, `_get_per_token_logps_and_entropies`, `_calculate_rewards`, `_prepare_inputs`, metric logging — is copied in full. **The tradeoff**: duplication is accepted, but **consistency is mandatory**. When the same logic appears in multiple trainers, the duplicated blocks must stay aligned: - Same variable names (`self._last_loaded_step`, `self._metrics[mode]`, …) - Same control flow structure (if/elif/else branches in the same order) - Same comments (word-for-word when the logic is identical) - Divergences only where the trainer's semantics require it (e.g., GRPO extracts logprobs from vLLM, RLOO discards them) **Consistency over correctness**: this is a strong requirement. When duplicating code, reproduce it exactly — even if you believe the original has a bug. Do not silently fix the issue in your copy. Instead, keep your copy consistent with the source and report the problem so it can be fixed across all trainers in a dedicated PR. A correct-but-inconsistent codebase is harder to maintain than a consistently-wrong one that can be fixed in a single sweep. **When modifying duplicated code**: if you change a pattern that exists in multiple trainers (e.g., the vLLM generation path in `_generate_single_turn`), apply the same change to all other trainers. A fix in GRPO often implies the same fix in RLOO, and vice versa. Not propagating a change is a bug. **When reviewing**: if a PR touches duplicated logic, verify that all copies are updated consistently. A common mistake is fixing one trainer and forgetting the others. ### Simplicity This codebase values **leanness and simplicity above all**. Prefer straightforward, inline code over abstractions, helpers, or utilities — even at the cost of some robustness or generality. Concretely: - Do not add layers of indirection (registries, factory patterns, plugin systems). A contributor should be able to read a trainer top to bottom and understand the full flow. - Prefer a simple implementation that covers 90% of cases over a complex one that covers 100%. A function that handles the common path in 20 lines is better than a catch-all that handles every edge case in 80. - Do not add defensive code, fallback paths, or configuration options "just in case". Only handle cases that actually exist today. - Avoid `hasattr` and `getattr`. Their use is almost always a symptom of overly defensive programming or a disguised version check (e.g., "this attribute was added in version X"). Instead, either drop the conditional entirely or express the version check explicitly with a version comparison. There is nearly always a cleaner alternative. - When in doubt, prefer less code. Every new function, parameter, or branch is maintenance burden. The best abstraction is often no abstraction. ## Documentation ### Docstrings Docstrings must follow the repository format below. Do **not** convert docstrings to other styles (Google, NumPy, etc.). Rules: * Types appear in backticks inside parentheses: (`str`) * Optional parameters are marked with `*optional*` * Defaults are written as: `defaults to ` * When the default is `None`, prefer ```(`str`, *optional*)``` instead of ```(`str` or `None`, *optional*, defaults to `None`)``` * Union types use `or`: `str` or `None` * References to classes use the format: [`~transformers.PreTrainedModel`] * Class docstrings may group parameters using headers such as: `> Parameters for X:` Example: ````python def method(self, param1: str, param2: int = 1, param3: float | None = None): """ Brief one-line description of what this does. Args: param1 (`str`): Description of required param. param2 (`int`, *optional*, defaults to `1`): Description of optional param with default. param3 (`float`, *optional*): Description of optional param without explicit default. Returns: `dict` with keys: - `key1` (`list[int]`): Description of this key. Examples: ```python >>> my_func("hello") ``` """ ```` ### Links to papers When linking to papers, use `https://huggingface.co/papers/` instead of `https://arxiv.org/abs/` (same ID suffix system). ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 title: 'TRL: Transformers Reinforcement Learning' message: >- If you use this software, please cite it using the metadata from this file. type: software authors: - given-names: Leandro family-names: von Werra - given-names: Younes family-names: Belkada - given-names: Lewis family-names: Tunstall - given-names: Edward family-names: Beeching - given-names: Tristan family-names: Thrush - given-names: Nathan family-names: Lambert - given-names: Shengyi family-names: Huang - given-names: Kashif family-names: Rasul - given-names: Quentin family-names: Gallouédec repository-code: 'https://github.com/huggingface/trl' abstract: >- TRL (Transformers Reinforcement Learning) is an open-source toolkit for aligning transformer models via post-training. It provides practical, scalable implementations of SFT, reward modeling, DPO, and GRPO within the Hugging Face ecosystem. keywords: - transformers - reinforcement learning - preference optimization - language model alignment - post-training license: Apache-2.0 version: '0.29' date-released: '2020-03-27' ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at feedback@huggingface.co. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: CONTRIBUTING.md ================================================ # How to contribute to TRL? Everyone is welcome to contribute, and we value everybody's contribution. Code contributions are not the only way to help the community. Answering questions, helping others, and improving the documentation are also immensely valuable. It also helps us if you spread the word! Reference the library in blog posts about the awesome projects it made possible, shout out on Twitter every time it has helped you, or simply ⭐️ the repository to say thank you. However you choose to contribute, please be mindful and respect our [code of conduct](https://github.com/huggingface/trl/blob/main/CODE_OF_CONDUCT.md). **This guide was heavily inspired by the awesome [scikit-learn guide to contributing](https://github.com/scikit-learn/scikit-learn/blob/main/CONTRIBUTING.md).** ## Ways to contribute There are several ways you can contribute to TRL: * Fix outstanding issues with the existing code. * Submit issues related to bugs or desired new features. * Implement trainers for new post-training algorithms. * Contribute to the examples or the documentation. If you don't know where to start, there is a special [Good First Issue](https://github.com/huggingface/trl/labels/%F0%9F%91%B6%20good%20first%20issue) listing. It will give you a list of open issues that are beginner-friendly and help you start contributing to open-source. The best way to do that is to open a Pull Request and link it to the issue that you'd like to work on. We try to give priority to opened PRs as we can easily track the progress of the fix, and if the contributor does not have time anymore, someone else can take the PR over. For something slightly more challenging, you can also take a look at the [Good Second Issue](https://github.com/huggingface/trl/labels/%F0%9F%A7%92%20good%20second%20issue) list. In general though, if you feel like you know what you're doing, go for it and we'll help you get there! 🚀 > All contributions are equally valuable to the community. 🥰 Before you start contributing make sure you have installed all the dev tools: ```bash pip install -e .[dev] ``` ## Fixing outstanding issues If you notice an issue with the existing code and have a fix in mind, feel free to [start contributing](#submitting-a-pull-request-pr) and open a Pull Request! ## Submitting a bug-related issue or feature request Do your best to follow these guidelines when submitting a bug-related issue or a feature request. It will make it easier for us to come back to you quickly and with good feedback. ### Did you find a bug? The TRL library is robust and reliable thanks to users who report the problems they encounter. Before you report an issue, we would really appreciate it if you could **make sure the bug was not already reported** (use the search bar on GitHub under Issues). Your issue should also be related to bugs in the library itself, and not your code. Once you've confirmed the bug hasn't already been reported, please include the following information in your issue so we can quickly resolve it: * Your **OS type and version**, **Python**, **PyTorch**, **TRL** and **Transformers** versions. * A short, self-contained, code snippet that allows us to reproduce the bug in less than 30s. * The *full* traceback if an exception is raised. * Attach any other additional information, like screenshots, you think may help. To get the OS and software versions automatically, run the following command: ```bash trl env ``` ### Do you want a new feature? If there is a new feature you'd like to see in TRL, please open an issue and describe: 1. What is the *motivation* behind this feature? Is it related to a problem or frustration with the library? Is it a feature related to something you need for a project? Is it something you worked on and think it could benefit the community? Whatever it is, we'd love to hear about it! 2. Describe your requested feature in as much detail as possible. The more you can tell us about it, the better we'll be able to help you. 3. Provide a *code snippet* that demonstrates the feature's usage. 4. If the feature is related to a paper, please include a link. If your issue is well written we're already 80% of the way there by the time you create it. ## Do you want to implement a new trainer? New post-training methods are published frequently and those that satisfy the following criteria are good candidates to be integrated into TRL: * **Simplicity:** Does the new method achieve similar performance as prior methods, but with less complexity? A good example is Direct Preference Optimization (DPO) [[Rafailov et al, 2023]](https://huggingface.co/papers/2305.18290), which provided a simpler and compelling alternative to RLHF methods. * **Efficiency:** Does the new method provide a significant improvement in training efficiency? A good example is Odds Ratio Preference Optimization (ORPO) [[Hong et al, 2023]](https://huggingface.co/papers/2403.07691), which utilizes a similar objective as DPO but requires half the GPU VRAM. Methods that only provide incremental improvements at the expense of added complexity or compute costs are unlikely to be included in TRL. If you want to implement a trainer for a new post-training method, first open an issue and provide the following information: * A short description of the method and a link to the paper. * Link to the implementation if it is open-sourced. * Link to model weights trained with the method if they are available. Based on the community and maintainer feedback, the next step will be to implement the trainer and config classes. See the following examples for inspiration: * Paired preference optimisation: [`dpo_trainer.py`](./trl/trainer/dpo_trainer.py) and [`dpo_config.py`](./trl/trainer/dpo_config.py) * RL-based optimisation: [`rloo_trainer.py`](./trl/trainer/rloo_trainer.py) and [`rloo_config.py`](./trl/trainer/rloo_config.py) * Online optimisation: [`online_dpo_trainer.py`](./trl/trainer/online_dpo_trainer.py) and [`online_dpo_config.py`](./trl/trainer/online_dpo_config.py) ## Do you want to add documentation? We're always looking for improvements to the documentation that make it more clear and accurate. Please let us know how the documentation can be improved, such as typos, dead links, and any missing, unclear, or inaccurate content... We'll be happy to make the changes or help you contribute if you're interested! ## Submitting a pull request (PR) Before writing code, we strongly advise you to search through the existing PRs or issues to make sure that nobody is already working on the same thing. If you are unsure, it is always a good idea to open an issue to get some feedback. You will need basic `git` proficiency to be able to contribute to TRL. `git` is not the easiest tool to use but it has the greatest manual. Type `git --help` in a shell and enjoy. If you prefer books, [Pro Git](https://git-scm.com/book/en/v2) is a very good reference. Follow these steps to start contributing: 1. Fork the [repository](https://github.com/huggingface/trl) by clicking on the 'Fork' button on the repository's page. This creates a copy of the code under your GitHub user account. 2. Clone your fork to your local disk, and add the base repository as a remote. The following command assumes you have your public SSH key uploaded to GitHub. See the following guide for more [information](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository). ```bash git clone git@github.com:/trl.git cd trl git remote add upstream https://github.com/huggingface/trl.git ``` 3. Create a new branch to hold your development changes, and do this for every new PR you work on. Start by synchronizing your `main` branch with the `upstream/main` branch (more details in the [GitHub Docs](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork)): ```bash git checkout main git fetch upstream git merge upstream/main ``` Once your `main` branch is synchronized, create a new branch from it: ```bash git checkout -b a-descriptive-name-for-my-changes ``` **Do not** work on the `main` branch. 4. Set up a development environment by running the following command in a conda or a virtual environment you've created for working on this library: ```bash pip install -e .[dev] ``` (If TRL was already installed in the virtual environment, remove it with `pip uninstall trl` before reinstalling it.) Alternatively, if you are using [Visual Studio Code](https://code.visualstudio.com/Download), the fastest way to get set up is by using the provided Dev Container. Check [the documentation on how to get started with dev containers](https://code.visualstudio.com/docs/remote/containers). 5. Develop the features on your branch. As you work on the features, you should make sure that the test suite passes. You should run the tests impacted by your changes like this (see below an explanation regarding the environment variable): ```bash pytest tests/.py ``` > For the following commands leveraging the `make` utility. You can also run the full suite with the following command. ```bash make test ``` TRL relies on `ruff` for maintaining consistent code formatting across its source files. Before submitting any PR, you should apply automatic style corrections and run code verification checks. We provide a `precommit` target in the `Makefile` that simplifies this process by running all required checks and optimizations on only the files modified by your PR. To apply these checks and corrections in one step, use: ```bash make precommit ``` This command runs the following: * Executes `pre-commit` hooks to automatically fix style issues with `ruff` and other tools. * Runs additional scripts such as adding copyright information. If you prefer to apply the style corrections separately or review them individually, the `pre-commit` hook will handle the formatting for the files in question. Once you're happy with your changes, add changed files using `git add` and make a commit with `git commit` to record your changes locally: ```bash git add modified_file.py git commit ``` Please write [good commit messages](https://chris.beams.io/posts/git-commit/). It is a good idea to sync your copy of the code with the original repository regularly. This way you can quickly account for changes: ```bash git fetch upstream git rebase upstream/main ``` Push the changes to your account using: ```bash git push -u origin a-descriptive-name-for-my-changes ``` 6. Once you are satisfied (**and the checklist below is happy too**), go to the webpage of your fork on GitHub. Click on 'Pull request' to send your changes to the project maintainers for review. 7. It's ok if maintainers ask you for changes. It happens to core contributors too! To ensure everyone can review your changes in the pull request, work on your local branch and push the updates to your fork. They will automatically appear in the pull request. ### Checklist 1. The title of your pull request should be a summary of its contribution; 2. If your pull request addresses an issue, please mention the issue number in the pull request description to make sure they are linked (and people consulting the issue know you are working on it); 3. To indicate a work in progress please prefix the title with `[WIP]`, or mark the PR as a draft PR. These are useful to avoid duplicated work, and to differentiate it from PRs ready to be merged; 4. Make sure existing tests pass; 5. Add high-coverage tests. No quality testing = no merge. ### Tests An extensive test suite is included to test the library behavior and several examples. Library tests can be found in the [tests folder](https://github.com/huggingface/trl/tree/main/tests). We use `pytest` to run the tests. From the root of the repository here's how to run tests with `pytest` for the library: ```bash python -m pytest -sv ./tests ``` That's how `make test` is implemented (without the `pip install` line)! You can specify a smaller set of tests to test only the feature you're working on. ### Default values guidelines 1. **Use defaults when appropriate**: Provide default values unless the parameter's value varies significantly by use case. For example, datasets or models should not have defaults, but parameters like `learning_rate` should. 2. **Prioritize proven defaults**: Default values should align with those recommended in the original paper or method. Alternatives require strong evidence of superior performance in most cases. 3. **Ensure safety and predictability**: Defaults must be safe, expected and reliable. Avoid settings that could lead to surprising outcomes, such as excessive memory usage or poor performance in edge cases. 4. **Balance consistency and flexibility**: Aim for consistent defaults across similar functions or methods. However, consistency should not be preferred to point 2 or 3. 5. **Opt-in for new features**: Do not enable new features or improvements (e.g., novel loss functions) by default. Users should explicitly opt-in to use these. ### Writing documentation High-quality documentation is crucial for maintaining a project that is easy to use, understand, and extend. When adding new features, ensure they are thoroughly documented to maintain consistency and clarity throughout the project. To illustrate what good documentation looks like, here’s an example of a well-documented function: ````python def replicate_str(string: str, n: int, sep: str = " ") -> str: r""" Replicate a string `n` times with a separator. Args: string (`str`): String to replicate. n (`int`): Number of times to replicate the string. sep (`str`, *optional*, defaults to `" "`): Separator to use between each replication. Returns: `str`: The replicated string. Examples: ```python >>> replicate_str("hello", 3) "hello hello hello" >>> replicate_str("hello", 3, sep=", ") "hello, hello, hello" ``` """ return sep.join([string] * n) ```` * **Line Wrapping:** Applied a consistent line wrap at column 120 to improve readability. * **Definite Articles:** Removed definite articles where possible to streamline language. (Eg: Changed "The string to replicate" to "String to replicate") * **Type Annotations:** * Always include type definitions, indicating if a parameter is optional and specifying the default value. * **String Defaults:** * Ensured that default string values are wrapped in double quotes: ```txt defaults to `"foo"` ``` * **Dictionary Typing:** * Replaced generic `dict` type hints with more explicit `dict[str, Any]` to clarify expected key-value pairs. * **Default Value Formatting:** * Consistently surrounded default values with backticks for improved formatting: ```txt defaults to `4` ``` * **Sub-sectioning:** When the number of arguments is large, consider breaking them into sub-sections for better readability. ```python def calculate_statistics(data: list[float], precision: int = 2, include_variance: bool = False) -> dict[str, float]: r""" Calculates basic statistics for a given dataset. Args: > Data inputs data (`list[float]`): A list of numerical values to analyze. > Configuration parameters precision (`int`, *optional*, defaults to `2`): Number of decimal places to round the results. include_variance (`bool`, *optional*, defaults to `False`): Whether to include the variance of the dataset in the results. Returns: `dict[str, float]`: A dictionary containing calculated statistics such as mean, median, and optionally variance. """ ... ``` ### Deprecation and backward compatibility Our approach to deprecation and backward compatibility is flexible and based on the feature’s usage and impact. Each deprecation is carefully evaluated, aiming to balance innovation with user needs. When a feature or component is marked for deprecation, its use will emit a warning message. This warning will include: * **Transition Guidance**: Instructions on how to migrate to the alternative solution or replacement. * **Removal Version**: The target version when the feature will be removed, providing users with a clear timeframe to transition. Example: ```python warnings.warn( "The `Trainer.foo` method is deprecated and will be removed in version 0.14.0. " "Please use the `Trainer.bar` class instead.", FutureWarning, stacklevel=2, ) ``` The deprecation and removal schedule is based on each feature's usage and impact, with examples at two extremes: * **Experimental or Low-Use Features**: For a feature that is experimental or has limited usage, backward compatibility may not be maintained between releases. Users should therefore anticipate potential breaking changes from one version to the next. * **Widely-Used Components**: For a feature with high usage, we aim for a more gradual transition period of approximately **5 months**, generally scheduling deprecation around **5 minor releases** after the initial warning. These examples represent the two ends of a continuum. The specific timeline for each feature will be determined individually, balancing innovation with user stability needs. ### Working with warnings Warnings play a critical role in guiding users toward resolving potential issues, but they should be used thoughtfully to avoid unnecessary noise. Unlike logging, which provides informational context or operational details, warnings signal conditions that require attention and action. Overusing warnings can dilute their importance, leading users to ignore them entirely. #### Definitions * **Correct**: An operation is correct if it is valid, follows the intended approach, and aligns with the current best practices or guidelines within the codebase. This is the recommended or intended way to perform the operation. * **Supported**: An operation is supported if it is technically valid and works within the current codebase, but it may not be the most efficient, optimal, or recommended way to perform the task. This includes deprecated features or legacy approaches that still work but may be phased out in the future. #### Choosing the right message * **Correct → No warning**: If the operation is fully valid and expected, no message should be issued. The system is working as intended, so no warning is necessary. * **Correct but deserves attention → No warning, possibly a log message**: When an operation is correct but uncommon or requires special attention, providing an informational message can be helpful. This keeps users informed without implying any issue. If available, use the logger to output this message. Example: ```python logger.info("This is an informational message about a rare but correct operation.") ``` * **Correct but very likely a mistake → Warning with option to disable**: In rare cases, you may want to issue a warning for a correct operation that’s very likely a mistake. In such cases, you must provide an option to suppress the warning. This can be done with a flag in the function. Example: ```python def my_function(foo, bar, _warn=True): if foo == bar: if _warn: logger.warning("foo and bar are the same, this is likely a mistake. Ignore this warning by setting `_warn=False`.") # Do something ``` * **Supported but not correct → Warning**: If the operation is technically supported but is deprecated, suboptimal, or could cause future issues (e.g., conflicting arguments), a warning should be raised. This message should be actionable, meaning it must explain how to resolve the issue. Example: ```python def my_function(foo, bar): if foo and bar: logger.warning("Both `foo` and `bar` were provided, but only one is allowed. Ignoring `foo`. Please pass only one of these arguments.") # Do something ``` * **Not supported → Exception**: If the operation is invalid or unsupported, raise an exception. This indicates that the operation cannot be performed and requires immediate attention. Example: ```python def my_function(foo, bar): if foo and bar: raise ValueError("Both `foo` and `bar` were provided, but only one is allowed. Please pass only one of these arguments.") ``` By following this classification, you ensure that warnings, information, and exceptions are used appropriately, providing clear guidance to the user without cluttering the system with unnecessary messages. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2020-2026 The HuggingFace Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST.in ================================================ include LICENSE include CONTRIBUTING.md include README.md include trl/accelerate_configs/*.yaml include trl/templates/*.md include trl/skills/**/*.md recursive-exclude * __pycache__ prune tests ================================================ FILE: MIGRATION.md ================================================ # Migrating from TRL v0 to v1 This guide covers the breaking changes introduced in TRL v1 and how to update your code. Most structural changes (trainers moved to experimental, removed model classes, etc.) already shipped in v0.29 — if you're already on v0.29, this migration is minimal. ## Changed defaults | Config | Parameter | v0 default | v1 default | Action needed | | --- | --- | --- | --- | --- | | `GRPOConfig` | `vllm_mode` | `"server"` | `"colocate"` | If you use `use_vllm=True` without specifying `vllm_mode`, vLLM will now run in the same process instead of connecting to a separate server. Set `vllm_mode="server"` explicitly if you rely on server mode. | | `RLOOConfig` | `vllm_mode` | `"server"` | `"colocate"` | Same as above. | ## Renamed options | Config | Parameter | v0 value | v1 value | Action needed | | --- | --- | --- | --- | --- | | `SFTConfig` | `packing` | `"bfd-requeue"` | `"bfd_split"` | Replace `packing="bfd-requeue"` with `packing="bfd_split"`. The old value will still be accepted for a few versions but will be removed in a future release. | ## Migrating from an earlier version Depending on which version you're migrating from, refer to the [release notes](https://github.com/huggingface/trl/releases) for v0.29 and earlier for version-specific changes. ================================================ FILE: Makefile ================================================ .PHONY: test precommit common_tests slow_tests tests_gpu test_experimental check_dirs := examples tests trl ACCELERATE_CONFIG_PATH = `pwd`/examples/accelerate_configs test: pytest -n auto -m "not slow and not low_priority" -s -v --reruns 5 --reruns-delay 1 --only-rerun '(OSError|Timeout|HTTPError.*502|HTTPError.*504||not less than or equal to 0.01)' tests precommit: python scripts/add_copyrights.py pre-commit run --all-files doc-builder style trl tests docs/source --max_len 119 slow_tests: pytest -m "slow" tests/ $(if $(IS_GITHUB_CI),--report-log "slow_tests.log",) test_experimental: pytest -n auto -s -v tests/experimental ================================================ FILE: README.md ================================================ # TRL - Transformers Reinforcement Learning
TRL Banner


A comprehensive library to post-train foundation models

License Documentation GitHub release Hugging Face Hub

## 🎉 What's New **OpenEnv Integration:** TRL now supports **[OpenEnv](https://huggingface.co/blog/openenv)**, the open-source framework from Meta for defining, deploying, and interacting with environments in reinforcement learning and agentic workflows. Explore how to seamlessly integrate TRL with OpenEnv in our [dedicated documentation](https://huggingface.co/docs/trl/openenv). ## Overview TRL is a cutting-edge library designed for post-training foundation models using advanced techniques like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), and Direct Preference Optimization (DPO). Built on top of the [🤗 Transformers](https://github.com/huggingface/transformers) ecosystem, TRL supports a variety of model architectures and modalities, and can be scaled-up across various hardware setups. ## Highlights - **Trainers**: Various fine-tuning methods are easily accessible via trainers like [`SFTTrainer`](https://huggingface.co/docs/trl/sft_trainer), [`GRPOTrainer`](https://huggingface.co/docs/trl/grpo_trainer), [`DPOTrainer`](https://huggingface.co/docs/trl/dpo_trainer), [`RewardTrainer`](https://huggingface.co/docs/trl/reward_trainer) and more. - **Efficient and scalable**: - Leverages [🤗 Accelerate](https://github.com/huggingface/accelerate) to scale from single GPU to multi-node clusters using methods like [DDP](https://pytorch.org/tutorials/intermediate/ddp_tutorial.html) and [DeepSpeed](https://github.com/deepspeedai/DeepSpeed). - Full integration with [🤗 PEFT](https://github.com/huggingface/peft) enables training on large models with modest hardware via quantization and LoRA/QLoRA. - Integrates [🦥 Unsloth](https://github.com/unslothai/unsloth) for accelerating training using optimized kernels. - **Command Line Interface (CLI)**: A simple interface lets you fine-tune with models without needing to write code. ## Installation ### Python Package Install the library using `pip`: ```bash pip install trl ``` ### From source If you want to use the latest features before an official release, you can install TRL from source: ```bash pip install git+https://github.com/huggingface/trl.git ``` ### Repository If you want to use the examples you can clone the repository with the following command: ```bash git clone https://github.com/huggingface/trl.git ``` ## Quick Start For more flexibility and control over training, TRL provides dedicated trainer classes to post-train language models or PEFT adapters on a custom dataset. Each trainer in TRL is a light wrapper around the 🤗 Transformers trainer and natively supports distributed training methods like DDP, DeepSpeed ZeRO, and FSDP. ### `SFTTrainer` Here is a basic example of how to use the [`SFTTrainer`](https://huggingface.co/docs/trl/sft_trainer): ```python from trl import SFTTrainer from datasets import load_dataset dataset = load_dataset("trl-lib/Capybara", split="train") trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B", train_dataset=dataset, ) trainer.train() ``` ### `GRPOTrainer` [`GRPOTrainer`](https://huggingface.co/docs/trl/grpo_trainer) implements the [Group Relative Policy Optimization (GRPO) algorithm](https://huggingface.co/papers/2402.03300) that is more memory-efficient than PPO and was used to train [Deepseek AI's R1](https://huggingface.co/deepseek-ai/DeepSeek-R1). ```python from datasets import load_dataset from trl import GRPOTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` > [!NOTE] > For reasoning models, use the `reasoning_accuracy_reward()` function for better results. ### `DPOTrainer` [`DPOTrainer`](https://huggingface.co/docs/trl/dpo_trainer) implements the popular [Direct Preference Optimization (DPO) algorithm](https://huggingface.co/papers/2305.18290) that was used to post-train [Llama 3](https://huggingface.co/papers/2407.21783) and many other models. Here is a basic example of how to use the `DPOTrainer`: ```python from datasets import load_dataset from trl import DPOTrainer dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = DPOTrainer( model="Qwen3/Qwen-0.6B", train_dataset=dataset, ) trainer.train() ``` ### `RewardTrainer` Here is a basic example of how to use the [`RewardTrainer`](https://huggingface.co/docs/trl/reward_trainer): ```python from trl import RewardTrainer from datasets import load_dataset dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = RewardTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=dataset, ) trainer.train() ``` ## Command Line Interface (CLI) You can use the TRL Command Line Interface (CLI) to quickly get started with post-training methods like Supervised Fine-Tuning (SFT) or Direct Preference Optimization (DPO): **SFT:** ```bash trl sft --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/Capybara \ --output_dir Qwen2.5-0.5B-SFT ``` **DPO:** ```bash trl dpo --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --dataset_name argilla/Capybara-Preferences \ --output_dir Qwen2.5-0.5B-DPO ``` Read more about CLI in the [relevant documentation section](https://huggingface.co/docs/trl/clis) or use `--help` for more details. ## Development If you want to contribute to `trl` or customize it to your needs make sure to read the [contribution guide](https://github.com/huggingface/trl/blob/main/CONTRIBUTING.md) and make sure you make a dev install: ```bash git clone https://github.com/huggingface/trl.git cd trl/ pip install -e .[dev] ``` ## Experimental A minimal incubation area is available under `trl.experimental` for unstable / fast-evolving features. Anything there may change or be removed in any release without notice. Example: ```python from trl.experimental.new_trainer import NewTrainer ``` Read more in the [Experimental docs](https://huggingface.co/docs/trl/experimental_overview). ## Citation ```bibtex @software{vonwerra2020trl, title = {{TRL: Transformers Reinforcement Learning}}, author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and Beeching, Edward and Thrush, Tristan and Lambert, Nathan and Huang, Shengyi and Rasul, Kashif and Gallouédec, Quentin}, license = {Apache-2.0}, url = {https://github.com/huggingface/trl}, year = {2020} } ``` ## License This repository's source code is available under the [Apache-2.0 License](LICENSE). ================================================ FILE: RELEASE.md ================================================ # Making a release > [!NOTE] > VERSION needs to be formatted following the `v{major}.{minor}.{patch}` convention. We need to follow this convention to be able to retrieve versioned scripts. ## Major/Minor Release ### 1. Ensure your local repository is up to date with the upstream repository ```bash git checkout main git pull origin main ``` > [!WARNING] > Do not merge other pull requests into `main` until the release is done. This is to ensure that the release is stable and does not include any untested changes. Announce internally (#trl-internal) to other maintainers that you are doing a release and that they must not merge PRs until the release is done. ### 2. Create a release branch from main ```bash git checkout -b release-v{major}.{minor} ``` ### 3. Change the version in the following files - `.github/workflows/tests_latest.yml`: ```diff - with: { ref: v{major}.{minor-1}-release } + with: { ref: v{major}.{minor}-release } ``` - `CITATION.cff` ```diff - version: '{major}.{minor-1}' + version: '{major}.{minor}' ``` - `VERSION` ```diff - {major}.{minor}.0.dev0 + {major}.{minor}.0 ``` ### 4. Commit and push these changes ```shell git add .github/workflows/tests_latest.yml CITATION.cff VERSION git commit -m 'Release: {major}.{minor}' git push origin release-v{major}.{minor} ``` ### 5. Create a pull request from `release-v{major}.{minor}` to `main`, named `Release: v{major}.{minor}`, wait for tests to pass, and request a review. ### 6. Once the pull request is approved, merge it into `main` It will automatically publish the new version of the package on PyPI. ### 7. Add a tag in git to mark the release ```shell git checkout main git pull origin main git tag -a v{major}.{minor}.0 -m 'Adds tag v{major}.{minor}.0 for PyPI' git push origin v{major}.{minor}.0 ``` ### 8. Create a branch `v{major}.{minor}-release` for future patch releases ```shell git checkout -b v{major}.{minor}-release git push origin v{major}.{minor}-release ``` This ensures that future patch releases (`v{major}.{minor}.1`, `v{major}.{minor}.2`, etc.) can be made separately from `main`. ### 9. Create a GitHub Release 1. Go to the repo’s [releases section](https://github.com/huggingface/trl/releases) on GitHub. 2. Click **Draft a new release**. 3. Select the `v{major}.{minor}.0` tag you just created in step 7. 4. Add a title (`v{major}.{minor}.0`) and a short description of what’s new. 5. Click **Publish Release**. ### 10. Bump to dev version 1. Create a branch `bump-dev-version-{major}.{minor+1}` from `main` and checkout to it. ```shell git checkout -b bump-dev-version-{major}.{minor+1} ``` 2. Change the version in file `VERSION`: ```diff - {major}.{minor}.0 + {major}.{minor+1}.0.dev0 ``` 3. Commit and push these changes ```shell git add VERSION git commit -m '⬆️ Bump dev version' git push origin bump-dev-version-{major}.{minor+1} ``` 4. Create a pull request from `bump-dev-version-{major}.{minor+1}` to `main`, named `⬆️ Bump dev version`, and request urgent review. 5. Once the pull request is approved, merge it into `main`. 6. The codebase is now ready for the next development cycle, inform the team in the #trl-internal channel. ## Making a patch release ### 1. Ensure your local repository is up to date with the upstream repository ```bash git checkout v{major}.{minor}-release git pull origin main ``` ### 2. Cherry-pick the changes you want to include in the patch release ```bash git cherry-pick git cherry-pick ... ``` ### 3. Change the version in the file `VERSION` ```diff - {major}.{minor}.{patch-1} + {major}.{minor}.{patch} ``` ### 4. Commit and push these changes ```shell git add VERSION git commit -m 'Release: {major}.{minor}.{patch}' git push origin v{major}.{minor}-release ``` ### 5. Wait for the CI to pass The CI will automatically publish the new version of the package on PyPI. ### 6. Add a tag in git to mark the release ```shell git tag -a v{major}.{minor}.{patch} -m 'Adds tag v{major}.{minor}.{patch} for PyPI' git push origin v{major}.{minor}.{patch} ``` #### 7. Create a GitHub Release 1. Go to the repo’s [releases section](https://github.com/huggingface/trl/releases) on GitHub. 2. Click **Draft a new release**. 3. Select the `v{major}.{minor}.{patch}` tag you just created in step 7. 4. Add a title (`v{major}.{minor}.{patch}`) and a short description of what’s new. 5. Click **Publish Release**. ================================================ FILE: VERSION ================================================ 1.0.0.dev0 ================================================ FILE: docker/trl/Dockerfile ================================================ FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* RUN pip install --upgrade pip uv RUN uv pip install --system trl[liger,peft,vlm] kernels trackio ================================================ FILE: docker/trl-dev/Dockerfile ================================================ FROM pytorch/pytorch:2.8.0-cuda12.8-cudnn9-devel RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* RUN pip install --upgrade pip uv RUN uv pip install --system --no-cache "git+https://github.com/huggingface/trl.git#egg=trl[liger,peft,vlm]" RUN uv pip install --system kernels liger_kernel peft trackio ================================================ FILE: docs/source/_toctree.yml ================================================ - sections: - local: index title: TRL - local: installation title: Installation - local: quickstart title: Quickstart title: Getting started - sections: - local: dataset_formats title: Dataset Formats - local: paper_index title: Paper Index title: Conceptual Guides - sections: # Sorted alphabetically - local: dpo_trainer title: DPO - local: grpo_trainer title: GRPO - local: reward_trainer title: Reward - local: rloo_trainer title: RLOO - local: sft_trainer title: SFT title: Trainers - sections: - local: clis title: Command Line Interface (CLI) - local: jobs_training title: Training using Jobs - local: customization title: Customizing the Training - local: reducing_memory_usage title: Reducing Memory Usage - local: speeding_up_training title: Speeding Up Training - local: distributing_training title: Distributing Training - local: use_model title: Using Trained Models title: How-to guides - sections: - local: deepspeed_integration title: DeepSpeed - local: kernels_hub title: Kernels Hub - local: liger_kernel_integration title: Liger Kernel - local: peft_integration title: PEFT - local: ptt_integration title: Post Training Toolkit - local: rapidfire_integration title: RapidFire AI - local: trackio_integration title: Trackio - local: unsloth_integration title: Unsloth - local: vllm_integration title: vLLM title: Integrations - sections: - local: example_overview title: Example Overview - local: community_tutorials title: Community Tutorials - local: lora_without_regret title: LoRA Without Regret title: Examples - sections: - sections: - local: chat_template_utils title: Chat Template Utilities - local: data_utils title: Data Utilities - local: script_utils title: Script Utilities title: Utilities - local: callbacks title: Callbacks - local: rewards title: Reward Functions title: API - sections: - local: experimental_overview title: Experimental Overview - local: openenv title: OpenEnv Integration - local: async_grpo_trainer # Sorted alphabetically title: Asynchronous GRPO - local: bema_for_reference_model title: BEMA for Reference Model - local: bco_trainer title: BCO - local: cpo_trainer title: CPO - local: gfpo title: GFPO - local: gkd_trainer title: GKD - local: gold_trainer title: GOLD - local: grpo_with_replay_buffer title: GRPO With Replay Buffer - local: gspo_token title: GSPO-token - local: judges title: Judges - local: kto_trainer title: KTO - local: merge_model_callback title: MergeModelCallback - local: minillm_trainer title: MiniLLM - local: nash_md_trainer title: Nash-MD - local: nemo_gym title: NeMo Gym - local: online_dpo_trainer title: Online DPO - local: orpo_trainer title: ORPO - local: papo_trainer title: PAPO - local: ppo_trainer title: PPO - local: prm_trainer title: PRM - local: winrate_callback title: WinRateCallback - local: xpo_trainer title: XPO title: Experimental ================================================ FILE: docs/source/async_grpo_trainer.md ================================================ # Asynchronous GRPO > [!IMPORTANT] > This trainer requires `vllm>=0.17.1` and `transformers>=5.2.0`. For distributed training, only FSDP2 is supported (DeepSpeed ZeRO is not). > > Currently, `vllm` and `transformers` have conflicting dependency constraints. To work around this, install vLLM first and then force-install transformers: > > ```bash > pip install 'vllm>=0.17.1' > pip install 'transformers>=5.2.0' --no-deps > ``` ## Overview [`AsyncGRPOTrainer`] implements the same [GRPO](grpo_trainer) algorithm but decouples rollout generation from training. A background worker continuously streams completions from a vLLM server while the training loop consumes them, so generation and gradient updates overlap instead of alternating. The API mirrors [`GRPOTrainer`] — for full details on the GRPO method itself (advantage computation, KL estimation, loss formulation, reward functions, etc.), see the [GRPO Trainer](grpo_trainer) documentation. Not all features from [`GRPOTrainer`] are available; refer to [`AsyncGRPOConfig`] for the supported parameters. This trainer was contributed by [Quentin Gallouédec](https://huggingface.co/qgallouedec) and [Amine Dirhoussi](https://huggingface.co/aminediroHF). ## How it differs from [`GRPOTrainer`] In the standard [`GRPOTrainer`], generation and training are sequential: generate a batch, compute the loss, update weights, repeat. Even in [vLLM colocate mode](grpo_trainer#speed-up-training-with-vllm), where generation runs on the same GPUs, one phase must finish before the other begins. [`AsyncGRPOTrainer`] separates these two concerns: - **Rollout worker** (background thread) — sends prompts to a vLLM server, scores completions with reward functions, computes advantages, and pushes ready-to-train samples into a queue. - **Training loop** (main process) — pulls samples from the queue, computes the clipped surrogate loss, and updates the model weights. After every `weight_sync_steps` training steps, the updated weights are transferred to the vLLM server via NCCL so that subsequent generations reflect the latest policy. Because generation and training run concurrently, the training samples may have been generated by a slightly older version of the model. The `max_staleness` parameter controls how many weight updates a sample can lag behind before being discarded. The number of concurrent requests sent to the vLLM server is controlled by `max_inflight_tasks`. By default it is set automatically to `max_staleness × per_device_train_batch_size × gradient_accumulation_steps × num_processes` — the maximum number of samples the trainer can consume before they become stale. Generating more than this is wasteful since the excess samples will be discarded. ## Quick start ```python # train_async_grpo.py from datasets import load_dataset from trl.experimental.async_grpo import AsyncGRPOTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = AsyncGRPOTrainer( model="Qwen/Qwen3-4B", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` The vLLM server and the trainer must run on **separate GPUs**. Use `CUDA_VISIBLE_DEVICES` to partition your GPUs. For example, with 2 GPUs, you can run the vLLM server on GPU 0 and the trainer on GPU 1 as follows: ```bash # Terminal 1: vLLM server on GPU 0 (dev mode + NCCL weight transfer are required) CUDA_VISIBLE_DEVICES=0 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3-4B \ --max-model-len 4096 \ --logprobs-mode processed_logprobs \ --weight-transfer-config '{"backend":"nccl"}' ``` > [!TIP] > Set `--max-model-len` to the maximum total sequence length (prompt + completion) you expect. A lower value reduces GPU memory usage on the server, freeing more memory for the KV cache and increasing throughput. A good starting point is the prompt length plus `max_completion_length` from your config. ```bash # Terminal 2: training on GPU 1 CUDA_VISIBLE_DEVICES=1 accelerate launch train_async_grpo.py ``` ## Design philosophy This trainer is intentionally kept minimal and is not meant to grow into a general-purpose solution. If you need a feature that is not supported, we recommend cloning the repository and adapting the trainer to your needs directly. New features will only be considered when there is significant community demand. ## AsyncGRPOConfig [[autodoc]] trl.experimental.async_grpo.AsyncGRPOConfig ## AsyncGRPOTrainer [[autodoc]] trl.experimental.async_grpo.AsyncGRPOTrainer ================================================ FILE: docs/source/bco_trainer.md ================================================ # BCO Trainer [![model badge](https://img.shields.io/badge/All_models-BCO-blue)](https://huggingface.co/models?other=bco,trl) TRL supports the Binary Classifier Optimization (BCO). The [BCO](https://huggingface.co/papers/2404.04656) authors train a binary classifier whose logit serves as a reward so that the classifier maps {prompt, chosen completion} pairs to 1 and {prompt, rejected completion} pairs to 0. For a full example have a look at [`examples/scripts/bco.py`]. ## Expected dataset type The [`experimental.bco.BCOTrainer`] requires an [unpaired preference dataset](dataset_formats#unpaired-preference). The [`experimental.bco.BCOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ## Expected model format The BCO trainer expects a model of `AutoModelForCausalLM`, compared to PPO that expects `AutoModelForCausalLMWithValueHead` for the value function. ## Using the `BCOTrainer` For a detailed example have a look at the `examples/scripts/bco.py` script. At a high level we need to initialize the `BCOTrainer` with a `model` we wish to train and a reference `ref_model` which we will use to calculate the implicit rewards of the preferred and rejected response. The `beta` refers to the hyperparameter of the implicit reward, and the dataset contains the 3 entries listed above. Note that the `model` and `ref_model` need to have the same architecture (ie decoder only or encoder-decoder). ```python from trl.experimental.bco import BCOConfig, BCOTrainer training_args = BCOConfig( beta=0.1, ) bco_trainer = BCOTrainer( model, model_ref, args=training_args, train_dataset=train_dataset, processing_class=tokenizer, ) ``` After this one can then call: ```python bco_trainer.train() ``` ## Underlying Distribution matching (UDM) In practical scenarios, the thumbs-up and thumbs-down datasets are likely to have divergent underlying distributions of prompts. Consider an LLM deployed for user feedback: if the model excels in writing tasks but underperforms in coding, the thumbs-up dataset will be dominated by writing-related prompts, while the thumbs-down dataset will contain mostly coding-related prompts. If the prompts in your desired and undesired datasets differ a lot, it is useful to enable UDM. Choose an embedding model and tokenizer: ```python embedding_model = AutoModel.from_pretrained(your_model_id) embedding_tokenizer = AutoTokenizer.from_pretrained(your_model_id) # customize this function depending on your embedding model def embed_prompt(input_ids, attention_mask, model): outputs = model(input_ids=input_ids, attention_mask=attention_mask) return outputs.last_hidden_state.mean(dim=1) embedding_model = Accelerator().prepare_model(self.embedding_model) embedding_func = partial(embed_prompt, model=embedding_model) ``` Set `prompt_sample_size` to define how many prompts are selected to train the UDM classifier and start the training with the provided embedding function: ```python training_args = BCOConfig( beta=0.1, prompt_sample_size=512, ) bco_trainer = BCOTrainer( model, model_ref, args=training_args, train_dataset=train_dataset, processing_class=tokenizer, embedding_func=embedding_func, embedding_tokenizer=self.embedding_tokenizer, ) bco_trainer.train() ``` ### For Mixture of Experts Models: Enabling the auxiliary loss MOEs are the most efficient if the load is about equally distributed between experts. To ensure that we train MOEs similarly during preference-tuning, it is beneficial to add the auxiliary loss from the load balancer to the final loss. This option is enabled by setting `output_router_logits=True` in the model config (e.g. MixtralConfig). To scale how much the auxiliary loss contributes to the total loss, use the hyperparameter `router_aux_loss_coef=...` (default: 0.001). ## BCOTrainer [[autodoc]] experimental.bco.BCOTrainer - train - save_model - push_to_hub ## BCOConfig [[autodoc]] experimental.bco.BCOConfig ================================================ FILE: docs/source/bema_for_reference_model.md ================================================ # BEMA for Reference Model This feature implements the BEMA algorithm to update the reference model during DPO training. ## Usage ```python from trl.experimental.bema_for_ref_model import BEMACallback, DPOTrainer from datasets import load_dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") bema_callback = BEMACallback(update_ref_model=True) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, callbacks=[bema_callback], ) trainer.train() ``` ## DPOTrainer [[autodoc]] experimental.bema_for_ref_model.DPOTrainer - train - save_model - push_to_hub ## BEMACallback [[autodoc]] experimental.bema_for_ref_model.BEMACallback ================================================ FILE: docs/source/callbacks.md ================================================ # Callbacks ## RichProgressCallback [[autodoc]] RichProgressCallback ## LogCompletionsCallback [[autodoc]] LogCompletionsCallback ## BEMACallback [[autodoc]] BEMACallback ## WeaveCallback [[autodoc]] WeaveCallback ================================================ FILE: docs/source/chat_template_utils.md ================================================ # Chat template utilities ## clone_chat_template [[autodoc]] clone_chat_template ## is_chat_template_prefix_preserving [[autodoc]] chat_template_utils.is_chat_template_prefix_preserving ## get_training_chat_template [[autodoc]] chat_template_utils.get_training_chat_template ================================================ FILE: docs/source/clis.md ================================================ # Command Line Interfaces (CLIs) TRL provides a powerful command-line interface (CLI) to fine-tune large language models (LLMs) using methods like Supervised Fine-Tuning (SFT), Direct Preference Optimization (DPO), and more. The CLI abstracts away much of the boilerplate, letting you launch training jobs quickly and reproducibly. ## Commands Currently supported commands are: ### Training Commands - `trl dpo`: fine-tune a LLM with DPO - `trl grpo`: fine-tune a LLM with GRPO - `trl kto`: fine-tune a LLM with KTO - `trl reward`: train a Reward Model - `trl rloo`: fine-tune a LLM with RLOO - `trl sft`: fine-tune a LLM with SFT ### Other Commands - `trl env`: get the system information - `trl vllm-serve`: serve a model with vLLM ## Fine-Tuning with the TRL CLI ### Basic Usage You can launch training directly from the CLI by specifying required arguments like the model and dataset: ```bash trl sft \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name stanfordnlp/imdb ``` ```bash trl dpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name anthropic/hh-rlhf ``` ```bash trl reward \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/ultrafeedback_binarized ``` ```bash trl grpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward ``` ```bash trl rloo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward ``` ```bash trl kto \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/kto-mix-14k ``` ### Using Configuration Files To keep your CLI commands clean and reproducible, you can define all training arguments in a YAML configuration file: ```yaml # sft_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: stanfordnlp/imdb ``` Launch with: ```bash trl sft --config sft_config.yaml ``` ```yaml # dpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: anthropic/hh-rlhf ``` Launch with: ```bash trl dpo --config dpo_config.yaml ``` ```yaml # reward_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/ultrafeedback_binarized ``` Launch with: ```bash trl reward --config reward_config.yaml ``` ```yaml # grpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward ``` Launch with: ```bash trl grpo --config grpo_config.yaml ``` ```yaml # rloo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward ``` Launch with: ```bash trl rloo --config rloo_config.yaml ``` ```yaml # kto_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/kto-mix-14k ``` Launch with: ```bash trl kto --config kto_config.yaml ``` ### Scaling Up with Accelerate TRL CLI natively supports [🤗 Accelerate](https://huggingface.co/docs/accelerate), making it easy to scale training across multiple GPUs, machines, or use advanced setups like DeepSpeed — all from the same CLI. You can pass any `accelerate launch` arguments directly to `trl`, such as `--num_processes`. For more information see [Using accelerate launch](https://huggingface.co/docs/accelerate/en/basic_tutorials/launch#using-accelerate-launch). ```bash trl sft \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name stanfordnlp/imdb \ --num_processes 4 ``` or, with a config file: ```yaml # sft_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: stanfordnlp/imdb num_processes: 4 ``` Launch with: ```bash trl sft --config sft_config.yaml ``` ```bash trl dpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name anthropic/hh-rlhf \ --num_processes 4 ``` or, with a config file: ```yaml # dpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: anthropic/hh-rlhf num_processes: 4 ``` Launch with: ```bash trl dpo --config dpo_config.yaml ``` ```bash trl reward \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/ultrafeedback_binarized \ --num_processes 4 ``` or, with a config file: ```yaml # reward_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/ultrafeedback_binarized num_processes: 4 ``` Launch with: ```bash trl reward --config reward_config.yaml ``` ```bash trl grpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward \ --num_processes 4 ``` or, with a config file: ```yaml # grpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward num_processes: 4 ``` Launch with: ```bash trl grpo --config grpo_config.yaml ``` ```bash trl rloo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward \ --num_processes 4 ``` or, with a config file: ```yaml # rloo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward num_processes: 4 ``` Launch with: ```bash trl rloo --config rloo_config.yaml ``` ```bash trl kto \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/kto-mix-14k \ --num_processes 4 ``` or, with a config file: ```yaml # kto_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/kto-mix-14k num_processes: 4 ``` Launch with: ```bash trl kto --config kto_config.yaml ``` ### Using `--accelerate_config` for Accelerate Configuration The `--accelerate_config` flag lets you easily configure distributed training with [🤗 Accelerate](https://github.com/huggingface/accelerate). This flag accepts either: - the name of a predefined config profile (built into TRL), or - a path to a custom Accelerate YAML config file. #### Predefined Config Profiles TRL provides several ready-to-use Accelerate configs to simplify common training setups: | Name | Description | | --- | --- | | `fsdp1` | Fully Sharded Data Parallel Stage 1 | | `fsdp2` | Fully Sharded Data Parallel Stage 2 | | `zero1` | DeepSpeed ZeRO Stage 1 | | `zero2` | DeepSpeed ZeRO Stage 2 | | `zero3` | DeepSpeed ZeRO Stage 3 | | `multi_gpu` | Multi-GPU training | | `single_gpu` | Single-GPU training | To use one of these, just pass the name to `--accelerate_config`. TRL will automatically load the corresponding config file from `trl/accelerate_config/`. #### Example Usage ```bash trl sft \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name stanfordnlp/imdb \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # sft_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: stanfordnlp/imdb accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl sft --config sft_config.yaml ``` ```bash trl dpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name anthropic/hh-rlhf \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # dpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: anthropic/hh-rlhf accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl dpo --config dpo_config.yaml ``` ```bash trl reward \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/ultrafeedback_binarized \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # reward_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/ultrafeedback_binarized accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl reward --config reward_config.yaml ``` ```bash trl grpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # grpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl grpo --config grpo_config.yaml ``` ```bash trl rloo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name HuggingFaceH4/Polaris-Dataset-53K \ --reward_funcs accuracy_reward \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # rloo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: HuggingFaceH4/Polaris-Dataset-53K reward_funcs: - accuracy_reward accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl rloo --config rloo_config.yaml ``` ```bash trl kto \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/kto-mix-14k \ --accelerate_config zero2 # or path/to/my/accelerate/config.yaml ``` or, with a config file: ```yaml # kto_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/kto-mix-14k accelerate_config: zero2 # or path/to/my/accelerate/config.yaml ``` Launch with: ```bash trl kto --config kto_config.yaml ``` ### Using dataset mixtures You can use dataset mixtures to combine multiple datasets into a single training dataset. This is useful for training on diverse data sources or when you want to mix different types of data. ```yaml # sft_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: stanfordnlp/imdb - path: roneneldan/TinyStories ``` Launch with: ```bash trl sft --config sft_config.yaml ``` ```yaml # dpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: BAAI/Infinity-Preference - path: argilla/Capybara-Preferences ``` Launch with: ```bash trl dpo --config dpo_config.yaml ``` ```yaml # reward_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: trl-lib/tldr-preference - path: trl-lib/lm-human-preferences-sentiment ``` Launch with: ```bash trl reward --config reward_config.yaml ``` ```yaml # grpo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: HuggingFaceH4/Polaris-Dataset-53K - path: trl-lib/DeepMath-103K reward_funcs: - accuracy_reward ``` Launch with: ```bash trl grpo --config grpo_config.yaml ``` ```yaml # rloo_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: HuggingFaceH4/Polaris-Dataset-53K - path: trl-lib/DeepMath-103K reward_funcs: - accuracy_reward ``` Launch with: ```bash trl rloo --config rloo_config.yaml ``` ```yaml # kto_config.yaml model_name_or_path: Qwen/Qwen2.5-0.5B datasets: - path: trl-lib/kto-mix-14k - path: argilla/ultrafeedback-binarized-preferences-cleaned ``` Launch with: ```bash trl kto --config kto_config.yaml ``` To see all the available keywords for defining dataset mixtures, refer to the [`scripts.utils.DatasetConfig`] and [`DatasetMixtureConfig`] classes. ## Getting the System Information You can get the system information by running the following command: ```bash trl env ``` This will print out the system information, including the GPU information, the CUDA version, the PyTorch version, the transformers version, the TRL version, and any optional dependencies that are installed. ```txt Copy-paste the following information when reporting an issue: - Platform: Linux-5.15.0-1048-aws-x86_64-with-glibc2.31 - Python version: 3.11.9 - PyTorch version: 2.4.1 - accelerator(s): NVIDIA H100 80GB HBM3 - Transformers version: 4.45.0.dev0 - Accelerate version: 0.34.2 - Accelerate config: - compute_environment: LOCAL_MACHINE - distributed_type: DEEPSPEED - mixed_precision: no - use_cpu: False - debug: False - num_processes: 4 - machine_rank: 0 - num_machines: 1 - rdzv_backend: static - same_network: True - main_training_function: main - enable_cpu_affinity: False - deepspeed_config: {'gradient_accumulation_steps': 4, 'offload_optimizer_device': 'none', 'offload_param_device': 'none', 'zero3_init_flag': False, 'zero_stage': 2} - downcast_bf16: no - tpu_use_cluster: False - tpu_use_sudo: False - tpu_env: [] - Datasets version: 3.0.0 - HF Hub version: 0.24.7 - TRL version: 0.12.0.dev0+acb4d70 - bitsandbytes version: 0.41.1 - DeepSpeed version: 0.15.1 - Diffusers version: 0.30.3 - Liger-Kernel version: 0.3.0 - LLM-Blender version: 0.0.2 - OpenAI version: 1.46.0 - PEFT version: 0.12.0 - vLLM version: not installed ``` This information is required when reporting an issue. ================================================ FILE: docs/source/community_tutorials.md ================================================ # Community Tutorials Community tutorials are made by active members of the Hugging Face community who want to share their knowledge and expertise with others. They are a great way to learn about the library and its features, and to get started with core classes and modalities. ## Language Models ### Tutorials | Task | Class | Description | Author | Tutorial | Colab | | --- | --- | --- | --- | --- | --- | | Reinforcement Learning | [`GRPOTrainer`] | Efficient Online Training with GRPO and vLLM in TRL | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/grpo_vllm_online_training) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/grpo_vllm_online_training.ipynb) | | Reinforcement Learning | [`GRPOTrainer`] | Post training an LLM for reasoning with GRPO in TRL | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_llm_grpo_trl) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_llm_grpo_trl.ipynb) | | Reinforcement Learning | [`GRPOTrainer`] | Mini-R1: Reproduce Deepseek R1 „aha moment“ a RL tutorial | [Philipp Schmid](https://huggingface.co/philschmid) | [Link](https://www.philschmid.de/mini-deepseek-r1) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/philschmid/deep-learning-pytorch-huggingface/blob/main/training/mini-deepseek-r1-aha-grpo.ipynb) | | Reinforcement Learning | [`GRPOTrainer`] | RL on LLaMA 3.1-8B with GRPO and Unsloth optimizations | [Andrea Manzoni](https://huggingface.co/AManzoni) | [Link](https://colab.research.google.com/github/amanzoni1/fine_tuning/blob/main/RL_LLama3_1_8B_GRPO.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/amanzoni1/fine_tuning/blob/main/RL_LLama3_1_8B_GRPO.ipynb) | | Instruction tuning | [`SFTTrainer`] | Fine-tuning Google Gemma LLMs using ChatML format with QLoRA | [Philipp Schmid](https://huggingface.co/philschmid) | [Link](https://www.philschmid.de/fine-tune-google-gemma) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/philschmid/deep-learning-pytorch-huggingface/blob/main/training/gemma-lora-example.ipynb) | | Structured Generation | [`SFTTrainer`] | Fine-tuning Llama-2-7B to generate Persian product catalogs in JSON using QLoRA and PEFT | [Mohammadreza Esmaeilian](https://huggingface.co/Mohammadreza) | [Link](https://huggingface.co/learn/cookbook/en/fine_tuning_llm_to_generate_persian_product_catalogs_in_json_format) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_llm_to_generate_persian_product_catalogs_in_json_format.ipynb) | | Preference Optimization | [`DPOTrainer`] | Align Mistral-7b using Direct Preference Optimization for human preference alignment | [Maxime Labonne](https://huggingface.co/mlabonne) | [Link](https://mlabonne.github.io/blog/posts/Fine_tune_Mistral_7b_with_DPO.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/mlabonne/llm-course/blob/main/Fine_tune_a_Mistral_7b_model_with_DPO.ipynb) | | Preference Optimization | [`experimental.orpo.ORPOTrainer`] | Fine-tuning Llama 3 with ORPO combining instruction tuning and preference alignment | [Maxime Labonne](https://huggingface.co/mlabonne) | [Link](https://mlabonne.github.io/blog/posts/2024-04-19_Fine_tune_Llama_3_with_ORPO.html) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1eHNWg9gnaXErdAa8_mcvjMupbSS6rDvi) | | Instruction tuning | [`SFTTrainer`] | How to fine-tune open LLMs in 2025 with Hugging Face | [Philipp Schmid](https://huggingface.co/philschmid) | [Link](https://www.philschmid.de/fine-tune-llms-in-2025) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/philschmid/deep-learning-pytorch-huggingface/blob/main/training/fine-tune-llms-in-2025.ipynb) | | Step-Level Reasoning | [`GRPOTrainer`] | Supervised Reinforcement Learning (SRL) for step-by-step reasoning with vLLM | [Deepak Swaminathan](https://huggingface.co/s23deepak) | [Link](https://github.com/s23deepak/Supervised-Reinforcement-Learning) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/s23deepak/Supervised-Reinforcement-Learning/blob/main/notebooks/srl_grpo_tutorial.ipynb) | ### Videos | Task | Title | Author | Video | | --- | --- | --- | --- | | Instruction tuning | Fine-tuning open AI models using Hugging Face TRL | [Wietse Venema](https://huggingface.co/wietsevenema) | [](https://youtu.be/cnGyyM0vOes) | | Instruction tuning | How to fine-tune a smol-LM with Hugging Face, TRL, and the smoltalk Dataset | [Mayurji](https://huggingface.co/iammayur) | [](https://youtu.be/jKdXv3BiLu0) |
⚠️ Deprecated features notice for "How to fine-tune a smol-LM with Hugging Face, TRL, and the smoltalk Dataset" (click to expand) > [!WARNING] > The tutorial uses two deprecated features: > > - `SFTTrainer(..., tokenizer=tokenizer)`: Use `SFTTrainer(..., processing_class=tokenizer)` instead, or simply omit it (it will be inferred from the model). > - `setup_chat_format(model, tokenizer)`: Use `SFTConfig(..., chat_template_path="Qwen/Qwen3-0.6B")`, where `chat_template_path` specifies the model whose chat template you want to copy.
## Vision Language Models ### Tutorials | Task | Class | Description | Author | Tutorial | Colab | | --- | --- | --- | --- | --- | --- | | Visual QA | [`SFTTrainer`] | Fine-tuning Qwen2-VL-7B for visual question answering on ChartQA dataset | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_vlm_trl) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_vlm_trl.ipynb) | | Visual QA | [`SFTTrainer`] | Fine-tuning SmolVLM with TRL on a consumer GPU | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_smol_vlm_sft_trl) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_smol_vlm_sft_trl.ipynb) | | SEO Description | [`SFTTrainer`] | Fine-tuning Qwen2-VL-7B for generating SEO-friendly descriptions from images | [Philipp Schmid](https://huggingface.co/philschmid) | [Link](https://www.philschmid.de/fine-tune-multimodal-llms-with-trl) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/philschmid/deep-learning-pytorch-huggingface/blob/main/training/fine-tune-multimodal-llms-with-trl.ipynb) | | Visual QA | [`DPOTrainer`] | PaliGemma 🤝 Direct Preference Optimization | [Merve Noyan](https://huggingface.co/merve) | [Link](https://github.com/merveenoyan/smol-vision/blob/main/PaliGemma_DPO.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/merveenoyan/smol-vision/blob/main/PaliGemma_DPO.ipynb) | | Visual QA | [`DPOTrainer`] | Fine-tuning SmolVLM using direct preference optimization (DPO) with TRL on a consumer GPU | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_vlm_dpo_smolvlm_instruct) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_vlm_dpo_smolvlm_instruct.ipynb) | | Object Detection Grounding | [`SFTTrainer`] | Fine tuning a VLM for Object Detection Grounding using TRL | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_vlm_object_detection_grounding) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_vlm_object_detection_grounding.ipynb) | | Visual QA | [`DPOTrainer`] | Fine-Tuning a Vision Language Model with TRL using MPO | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_vlm_mpo) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_vlm_mpo.ipynb) | | Reinforcement Learning | [`GRPOTrainer`] | Post training a VLM for reasoning with GRPO using TRL | [Sergio Paniego](https://huggingface.co/sergiopaniego) | [Link](https://huggingface.co/learn/cookbook/fine_tuning_vlm_grpo_trl) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/cookbook/blob/main/notebooks/en/fine_tuning_vlm_grpo_trl.ipynb) | ## Speech Language Models ### Tutorials | Task | Class | Description | Author | Tutorial | | --- | --- | --- | --- | --- | | Text-to-Speech | [`GRPOTrainer`] | Post training a Speech Language Model with GRPO using TRL | [Steven Zheng](https://huggingface.co/Steveeeeeeen) | [Link](https://huggingface.co/blog/Steveeeeeeen/llasa-grpo) | ## Contributing If you have a tutorial that you would like to add to this list, please open a PR to add it. We will review it and merge it if it is relevant to the community. ================================================ FILE: docs/source/cpo_trainer.md ================================================ # CPO Trainer [![model badge](https://img.shields.io/badge/All_models-CPO-blue)](https://huggingface.co/models?other=cpo,trl) ## Overview Contrastive Preference Optimization (CPO) as introduced in the paper [Contrastive Preference Optimization: Pushing the Boundaries of LLM Performance in Machine Translation](https://huggingface.co/papers/2401.08417) by [Haoran Xu](https://huggingface.co/haoranxu), [Amr Sharaf](https://huggingface.co/amrsharaf), [Yunmo Chen](https://huggingface.co/yunmochen), Weiting Tan, Lingfeng Shen, Benjamin Van Durme, [Kenton Murray](https://huggingface.co/Kenton), and [Young Jin Kim](https://huggingface.co/ykim362). At a high level, CPO trains models to avoid generating adequate, but not perfect, translations in Machine Translation (MT) tasks. However, CPO is a general approximation of the DPO loss and can be applied to other domains, such as chat. CPO aims to mitigate two fundamental shortcomings of SFT. First, SFT’s methodology of minimizing the discrepancy between predicted outputs and gold-standard references inherently caps model performance at the quality level of the training data. Secondly, SFT lacks a mechanism to prevent the model from rejecting mistakes in translations. The CPO objective is derived from the DPO objective. ## Quick start This example demonstrates how to train a model using the CPO method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model. We use the preference data from the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). You can view the data in the dataset here: Below is the script to train the model: ```python # train_cpo.py from datasets import load_dataset from trl.experimental.cpo import CPOConfig, CPOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") train_dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") training_args = CPOConfig(output_dir="Qwen2-0.5B-CPO") trainer = CPOTrainer(model=model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_cpo.py ``` ## Expected dataset type CPO requires a [preference dataset](dataset_formats#preference). The [`experimental.cpo.CPOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ## Example script We provide an example script to train a model using the CPO method. The script is available in [`examples/scripts/cpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/cpo.py) To test the CPO script with the [Qwen2 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/trl-lib/ultrafeedback_binarized), run the following command: ```bash accelerate launch examples/scripts/cpo.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --num_train_epochs 1 \ --output_dir Qwen2-0.5B-CPO ``` ## Logged metrics While training and evaluating, we record the following reward metrics: * `rewards/chosen`: the mean log probabilities of the policy model for the chosen responses scaled by beta * `rewards/rejected`: the mean log probabilities of the policy model for the rejected responses scaled by beta * `rewards/accuracies`: mean of how often the chosen rewards are > than the corresponding rejected rewards * `rewards/margins`: the mean difference between the chosen and corresponding rejected rewards * `nll_loss`: the mean negative log likelihood loss of the policy model for the chosen responses ## CPO variants ### Simple Preference Optimization (SimPO) [Simple Preference Optimization](https://huggingface.co/papers/2405.14734) (SimPO) by [Yu Meng](https://huggingface.co/yumeng5), [Mengzhou Xia](https://huggingface.co/mengzhouxia), and [Danqi Chen](https://huggingface.co/cdq10131) proposes a simpler and more effective preference optimization algorithm than DPO without using a reference model. The key designs in SimPO are (1) using length-normalized log likelihood as the implicit reward, and (2) incorporating a target reward margin in the Bradley-Terry ranking objective. The official code can be found at [princeton-nlp/SimPO](https://github.com/princeton-nlp/SimPO). The abstract from the paper is the following: > Direct Preference Optimization (DPO) is a widely used offline preference optimization algorithm that reparameterizes reward functions in reinforcement learning from human feedback (RLHF) to enhance simplicity and training stability. In this work, we propose SimPO, a simpler yet more effective approach. The effectiveness of SimPO is attributed to a key design: using the average log probability of a sequence as the implicit reward. This reward formulation better aligns with model generation and eliminates the need for a reference model, making it more compute and memory efficient. Additionally, we introduce a target reward margin to the Bradley-Terry objective to encourage a larger margin between the winning and losing responses, further enhancing the algorithm's performance. We compare SimPO to DPO and its latest variants across various state-of-the-art training setups, including both base and instruction-tuned models like Mistral and Llama3. We evaluated on extensive instruction-following benchmarks, including AlpacaEval 2, MT-Bench, and the recent challenging Arena-Hard benchmark. Our results demonstrate that SimPO consistently and significantly outperforms existing approaches without substantially increasing response length. Specifically, SimPO outperforms DPO by up to 6.4 points on AlpacaEval 2 and by up to 7.5 points on Arena-Hard. Our top-performing model, built on Llama3-8B-Instruct, achieves a remarkable 44.7 length-controlled win rate on AlpacaEval 2 -- surpassing Claude 3 Opus on the leaderboard, and a 33.8 win rate on Arena-Hard -- making it the strongest 8B open-source model. The SimPO loss is integrated in the [`experimental.cpo.CPOTrainer`], as it's an alternative loss that adds a reward margin, allows for length normalization, and does not use BC regularization. To use this loss, just turn on `loss_type="simpo"` and `cpo_alpha=0.0` in the [`experimental.cpo.CPOConfig`] and set the `simpo_gamma` to a recommended value. ### CPO-SimPO We also offer the combined use of CPO and SimPO, which enables more stable training and improved performance. Learn more details at [CPO-SimPO GitHub](https://github.com/fe1ixxu/CPO_SIMPO). To use this method, simply enable SimPO by setting `loss_type="simpo"` and a non-zero `cpo_alpha` in the [`experimental.cpo.CPOConfig`]. ### AlphaPO The [AlphaPO -- Reward shape matters for LLM alignment](https://huggingface.co/papers/2501.03884) (AlphaPO) method by Aman Gupta, Shao Tang, Qingquan Song, Sirou Zhu, [Jiwoo Hong](https://huggingface.co/JW17), Ankan Saha, Viral Gupta, Noah Lee, Eunki Kim, Jason Zhu, Natesh Pillai, and S. Sathiya Keerthi is also implemented in the [`experimental.cpo.CPOTrainer`]. AlphaPO is an alternative method that applies a transformation to the reward function shape in the context of SimPO loss. The abstract from the paper is the following: > Reinforcement Learning with Human Feedback (RLHF) and its variants have made huge strides toward the effective alignment of large language models (LLMs) to follow instructions and reflect human values. More recently, Direct Alignment Algorithms (DAAs) have emerged in which the reward modeling stage of RLHF is skipped by characterizing the reward directly as a function of the policy being learned. Some popular examples of DAAs include Direct Preference Optimization (DPO) and Simple Preference Optimization (SimPO). These methods often suffer from likelihood displacement, a phenomenon by which the probabilities of preferred responses are often reduced undesirably. In this paper, we argue that, for DAAs the reward (function) shape matters. We introduce AlphaPO, a new DAA method that leverages an α-parameter to help change the shape of the reward function beyond the standard log reward. AlphaPO helps maintain fine-grained control over likelihood displacement and overoptimization. Compared to SimPO, one of the best performing DAAs, AlphaPO leads to about 7% to 10% relative improvement in alignment performance for the instruct versions of Mistral-7B and Llama3-8B while achieving 15% to 50% relative improvement over DPO on the same models. The analysis and results presented highlight the importance of the reward shape and how one can systematically change it to affect training dynamics, as well as improve alignment performance. To use this loss as described in the paper, we can set the `loss_type="alphapo"` which automatically sets `loss_type="simpo"` and `cpo_alpha=0.0`, together with `alpha` and `simpo_gamma` to recommended values in the [`experimental.cpo.CPOConfig`]. Alternatively, you can manually set `loss_type="simpo"`, `cpo_alpha=0.0`, together with `alpha` and `simpo_gamma` to recommended values. Other variants of this method are also possible, such as setting `loss_type="ipo"` and `alpha` to any non-zero value. ## Loss functions The CPO algorithm supports several loss functions. The loss function can be set using the `loss_type` parameter in the [`experimental.cpo.CPOConfig`]. The following loss functions are supported: | `loss_type=` | Description | | --- | --- | | `"sigmoid"` (default) | Given the preference data, we can fit a binary classifier according to the Bradley-Terry model, and in fact, the [DPO](https://huggingface.co/papers/2305.18290) authors propose the sigmoid loss on the normalized likelihood via the `logsigmoid` to fit a logistic regression. | | `"hinge"` | The [RSO](https://huggingface.co/papers/2309.06657) authors propose to use a hinge loss on the normalized likelihood from the [SLiC](https://huggingface.co/papers/2305.10425) paper. In this case, the `beta` is the reciprocal of the margin. | | `"ipo"` | The [IPO](https://huggingface.co/papers/2310.12036) authors provide a deeper theoretical understanding of the DPO algorithms and identify an issue with overfitting and propose an alternative loss. In this case, the `beta` is the reciprocal of the gap between the log-likelihood ratios of the chosen vs the rejected completion pair, and thus the smaller the `beta`, the larger this gap is. As per the paper, the loss is averaged over log-likelihoods of the completion (unlike DPO, which is summed only). | | `"simpo"` | The [SimPO](https://huggingface.co/papers/2405.14734) method is also implemented in the [`experimental.cpo.CPOTrainer`]. SimPO is an alternative loss that adds a reward margin, allows for length normalization, and does not use BC regularization. To use this loss, simply set `loss_type="simpo"` and `cpo_alpha=0.0` in the [`experimental.cpo.CPOConfig`] and `simpo_gamma` to a recommended value. | | `"alphapo"` | The [AlphaPO](https://huggingface.co/papers/2501.03884) method is also implemented in the [`experimental.cpo.CPOTrainer`]. This is syntactic sugar that automatically sets `loss_type="simpo"` and `cpo_alpha=0.0`. AlphaPO applies a transformation to the reward function shape in the context of SimPO loss when the `alpha` parameter is non-zero. | ### For Mixture of Experts Models: Enabling the auxiliary loss MOEs are the most efficient if the load is about equally distributed between experts. To ensure that we train MOEs similarly during preference-tuning, it is beneficial to add the auxiliary loss from the load balancer to the final loss. This option is enabled by setting `output_router_logits=True` in the model config (e.g., [`~transformers.MixtralConfig`]). To scale how much the auxiliary loss contributes to the total loss, use the hyperparameter `router_aux_loss_coef=...` (default: `0.001`) in the model config. ## CPOTrainer [[autodoc]] experimental.cpo.CPOTrainer - train - save_model - push_to_hub ## CPOConfig [[autodoc]] experimental.cpo.CPOConfig ================================================ FILE: docs/source/customization.md ================================================ # Training customization TRL is designed with modularity in mind so that users are able to efficiently customize the training loop for their needs. Below are examples on how you can apply and test different techniques. > [!NOTE] > Although these examples use the [`DPOTrainer`], these customization methods apply to most (if not all) trainers in TRL. ## Use different optimizers and schedulers By default, the [`DPOTrainer`] creates a `torch.optim.AdamW` optimizer. You can create and define a different optimizer and pass it to [`DPOTrainer`] as follows: ```python from datasets import load_dataset from torch import optim from transformers import AutoModelForCausalLM from trl import DPOTrainer dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct") optimizer = optim.SGD(model.parameters(), lr=1e-6) trainer = DPOTrainer( model=model, train_dataset=dataset, optimizers=(optimizer, None), ) trainer.train() ``` ### Add a learning rate scheduler You can also add learning rate schedulers by passing both optimizer and scheduler: ```python from torch import optim optimizer = optim.AdamW(model.parameters(), lr=1e-6) lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1) trainer = DPOTrainer(..., optimizers=(optimizer, lr_scheduler)) ``` ## Pass 8-bit reference models Since `trl` supports all keyword arguments when loading a model from `transformers` using `from_pretrained`, you can also leverage `load_in_8bit` from `transformers` for more memory efficient fine-tuning. Read more about 8-bit model loading in `transformers` [Load in 8bit or 4bit](https://huggingface.co/docs/transformers/en/peft). ```python from transformers import AutoModelForCausalLM, BitsAndBytesConfig quantization_config = BitsAndBytesConfig(load_in_8bit=True) ref_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", quantization_config=quantization_config) trainer = DPOTrainer(..., ref_model=ref_model) ``` ## Add custom callbacks You can customize the training loop by adding callbacks for logging, monitoring, or early stopping. Callbacks allow you to execute custom code at specific points during training. ```python from transformers import TrainerCallback class CustomLoggingCallback(TrainerCallback): def on_log(self, args, state, control, logs=None, **kwargs): if logs is not None: print(f"Step {state.global_step}: {logs}") trainer = DPOTrainer(..., callbacks=[CustomLoggingCallback()]) ``` ## Add custom evaluation metrics You can define custom evaluation metrics to track during training. This is useful for monitoring model performance on specific tasks. ```python def compute_metrics(eval_preds): logits, labels = eval_preds # Add your metric computation here return {"custom_metric": 0.0} training_args = DPOConfig(..., eval_strategy="steps", eval_steps=100) trainer = DPOTrainer(..., eval_dataset=eval_dataset, compute_metrics=compute_metrics) ``` ## Use mixed precision training Mixed precision training can significantly speed up training and reduce memory usage. You can enable it by setting `bf16=True` or `fp16=True` in the training config. ```python # Use bfloat16 precision (recommended for modern GPUs) training_args = DPOConfig(..., bf16=True) ``` Note: Use `bf16=True` for Ampere GPUs (A100, RTX 30xx) or newer, and `fp16=True` for older GPUs. ## Use gradient accumulation When training with limited GPU memory, gradient accumulation allows you to simulate larger batch sizes by accumulating gradients over multiple steps before updating weights. ```python # Simulate a batch size of 32 with per_device_train_batch_size=4 and gradient_accumulation_steps=8 training_args = DPOConfig( ..., per_device_train_batch_size=4, gradient_accumulation_steps=8, ) ``` ================================================ FILE: docs/source/data_utils.md ================================================ # Data Utilities ## is_conversational [[autodoc]] is_conversational ## maybe_convert_to_chatml [[autodoc]] maybe_convert_to_chatml ## extract_prompt [[autodoc]] extract_prompt ## unpair_preference_dataset [[autodoc]] unpair_preference_dataset ================================================ FILE: docs/source/dataset_formats.md ================================================ # Dataset formats and types This guide provides an overview of the dataset formats and types supported by each trainer in TRL. ## Overview of the dataset formats and types - The *format* of a dataset refers to how the data is structured, typically categorized as either *standard* or *conversational*. - The *type* is associated with the specific task the dataset is designed for, such as *prompt-only* or *preference*. Each type is characterized by its columns, which vary according to the task, as shown in the table.
Type \ Format Standard Conversational
Language modeling
{"text": "The sky is blue."}
{"messages": [{"role": "user", "content": "What color is the sky?"},
              {"role": "assistant", "content": "It is blue."}]}
Prompt-only
{"prompt": "The sky is"}
{"prompt": [{"role": "user", "content": "What color is the sky?"}]}
Prompt-completion
{"prompt": "The sky is",
 "completion": " blue."}
{"prompt": [{"role": "user", "content": "What color is the sky?"}],
 "completion": [{"role": "assistant", "content": "It is blue."}]}
Preference
{"prompt": "The sky is",
 "chosen": " blue.",
 "rejected": " green."}
or, with implicit prompt:
{"chosen": "The sky is blue.",
 "rejected": "The sky is green."}
{"prompt": [{"role": "user", "content": "What color is the sky?"}],
 "chosen": [{"role": "assistant", "content": "It is blue."}],
 "rejected": [{"role": "assistant", "content": "It is green."}]}
or, with implicit prompt:
{"chosen": [{"role": "user", "content": "What color is the sky?"},
              {"role": "assistant", "content": "It is blue."}],
 "rejected": [{"role": "user", "content": "What color is the sky?"},
                {"role": "assistant", "content": "It is green."}]}
Unpaired preference
{"prompt": "The sky is",
 "completion": " blue.",
 "label": True}
{"prompt": [{"role": "user", "content": "What color is the sky?"}],
 "completion": [{"role": "assistant", "content": "It is green."}],
 "label": False}
Stepwise supervision
{"prompt": "Which number is larger, 9.8 or 9.11?",
 "completions": ["The fractional part of 9.8 is 0.8.",
                 "The fractional part of 9.11 is 0.11.",
                 "0.11 is greater than 0.8.",
                 "Hence, 9.11 > 9.8."],
 "labels": [True, True, False, False]}
### Formats #### Standard The standard dataset format typically consists of plain text strings. The columns in the dataset vary depending on the task. This is the format expected by TRL trainers. Below are examples of standard dataset formats for different tasks: ```python # Language modeling language_modeling_example = {"text": "The sky is blue."} # Preference preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."} # Unpaired preference unpaired_preference_example = {"prompt": "The sky is", "completion": " blue.", "label": True} ``` #### Conversational Conversational datasets are used for tasks involving dialogues or chat interactions between users and assistants. Unlike standard dataset formats, these contain sequences of messages where each message has a `role` (e.g., `"user"` or `"assistant"`) and `content` (the message text). ```python messages = [ {"role": "user", "content": "Hello, how are you?"}, {"role": "assistant", "content": "I'm doing great. How can I help you today?"}, {"role": "user", "content": "I'd like to show off how chat templating works!"}, ] ``` Just like standard datasets, the columns in conversational datasets vary depending on the task. Below are examples of conversational dataset formats for different tasks: ```python # Prompt-completion prompt_completion_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}]} # Preference preference_example = { "prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}], } ``` #### Tool Calling Some chat templates support *tool calling*, which allows the model to interact with external functions—referred to as **tools**—during generation. This extends the conversational capabilities of the model by enabling it to output a `"tool_calls"` field instead of a standard `"content"` message whenever it decides to invoke a tool. After the assistant initiates a tool call, the tool executes and returns its output. The assistant can then process this output and continue the conversation accordingly. Here’s a simple example of a tool-calling interaction: ```python messages = [ {"role": "user", "content": "Turn on the living room lights."}, {"role": "assistant", "tool_calls": [ {"type": "function", "function": { "name": "control_light", "arguments": {"room": "living room", "state": "on"} }}] }, {"role": "tool", "name": "control_light", "content": "The lights in the living room are now on."}, {"role": "assistant", "content": "Done!"} ] ``` When preparing datasets for Supervised Fine-Tuning (SFT) with tool calling, it is important that your dataset includes an additional column named `tools`. This column contains the list of available tools for the model, which is usually used by the chat template to construct the system prompt. The tools must be specified in a codified JSON schema format. You can automatically generate this schema from Python function signatures using the [`~transformers.utils.get_json_schema`] utility: ```python import json from transformers.utils import get_json_schema def control_light(room: str, state: str) -> str: """ Controls the lights in a room. Args: room: The name of the room. state: The desired state of the light ("on" or "off"). Returns: str: A message indicating the new state of the lights. """ return f"The lights in {room} are now {state}." # Generate JSON schema json_schema = get_json_schema(control_light) ``` The generated schema would look like: ```python {"type": "function", "function": {"name": "control_light", "description": "Controls the lights in a room.", "parameters": {"type": "object", "properties": {"room": {"type": "string", "description": "The name of the room."}, "state": {"type": "string", "description": "The desired state of the light (\"on\" or \"off\")."}}, "required": ["room", "state"]}, "return": {"type": "string", "description": "str: A message indicating the new state of the lights."}}} ``` A complete dataset entry for SFT might look like: ```python {"messages": messages, "tools": [json_schema]} ``` To get a `Dataset` you need to use the `Json()` type for tool arguments since they are arbitrary JSON objects, and not dictionaries with fixed fields and types: ```python from datasets import Dataset data = [ {"messages": messages1, "tools": [json_schema1]}, {"messages": messages2, "tools": [json_schema2]}, ] # auto-apply the Json() type dataset = Dataset.from_list(data, on_mixed_types="use_json") # or specify the features manually from datasets import Features, Json, List, Value features = Features( { "messages": List({"role": Value("string"), "content": Value("string"), "tool_calls": List(Json())}), "tools": List(Json()), } ) dataset = Dataset.from_list(data, features=features) ``` On older versions of `datasets` (<4.7.0) that don't have the `Json()` type, you should store `tools` as a JSON `str` (with `json.dumps([...])`): ```python dataset = Dataset.from_list( [{"messages": messages1, "tools": json.dumps([json_schema1])}, {"messages": messages2, "tools": json.dumps([json_schema2])}] ) ``` For more detailed information on tool calling, refer to the [Tool Calling section in the `transformers` documentation](https://huggingface.co/docs/transformers/chat_extras#tools-and-rag) and the blog post [Tool Use, Unified](https://huggingface.co/blog/unified-tool-use). ### Harmony The [Harmony response format](https://cookbook.openai.com/articles/openai-harmony) was introduced with the [OpenAI GPT OSS models](https://huggingface.co/collections/openai/gpt-oss-68911959590a1634ba11c7a4). It extends the conversational format by adding richer structure for reasoning, function calls, and metadata about the model’s behavior. Key features include: - **Developer role** – Provides high level instructions (similar to a system prompt) and lists available tools. - **Channels** – Separate types of assistant output into distinct streams: - `analysis` – for internal reasoning, from the key `"thinking"` - `final` – for the user-facing answer, from the key `"content"` - `commentary` – for tool calls or meta notes - **Reasoning effort** – Signals how much thinking the model should show (e.g., `"low"`, `"medium"`, `"high"`). - **Model identity** – Explicitly defines the assistant’s persona. ```python from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("openai/gpt-oss-20b") messages = [ {"role": "developer", "content": "Use a friendly tone."}, {"role": "user", "content": "What is the meaning of life?"}, {"role": "assistant", "thinking": "Deep reflection...", "content": "The final answer is..."}, ] print( tokenizer.apply_chat_template( messages, tokenize=False, reasoning_effort="low", model_identity="You are HuggingGPT, a large language model trained by Hugging Face.", ) ) ``` This produces: ```txt <|start|>system<|message|>You are HuggingGPT, a large language model trained by Hugging Face. Knowledge cutoff: 2024-06 Current date: 2025-08-03 Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Use a friendly tone.<|end|><|start|>user<|message|>What is the meaning of life?<|end|><|start|>assistant<|channel|>analysis<|message|>Deep reflection...<|end|><|start|>assistant<|channel|>final<|message|>The final answer is...<|return|> ``` For full details on message structure, supported fields, and advanced usage, see the [Harmony documentation](https://cookbook.openai.com/articles/openai-harmony). ### Types #### Language modeling A language modeling dataset consists of a column `"text"` (or `"messages"` for conversational datasets) containing a full sequence of text. ```python # Standard format language_modeling_example = {"text": "The sky is blue."} # Conversational format language_modeling_example = {"messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."} ]} ``` #### Prompt-only In a prompt-only dataset, only the initial prompt (the question or partial sentence) is provided under the key `"prompt"`. The training typically involves generating completion based on this prompt, where the model learns to continue or complete the given input. ```python # Standard format prompt_only_example = {"prompt": "The sky is"} # Conversational format prompt_only_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}]} ``` For examples of prompt-only datasets, refer to the [Prompt-only datasets collection](https://huggingface.co/collections/trl-lib/prompt-only-datasets-677ea25245d20252cea00368). > [!TIP] > While both the prompt-only and language modeling types are similar, they differ in how the input is handled. In the prompt-only type, the prompt represents a partial input that expects the model to complete or continue, while in the language modeling type, the input is treated as a complete sentence or sequence. These two types are processed differently by TRL. Below is an example showing the difference in the output of the `apply_chat_template` function for each type: > > ```python > from transformers import AutoTokenizer > from trl import apply_chat_template > > tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-128k-instruct") > > # Example for prompt-only type > prompt_only_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}]} > apply_chat_template(prompt_only_example, tokenizer) > # Output: {'prompt': '<|user|>\nWhat color is the sky?<|end|>\n<|assistant|>\n'} > > # Example for language modeling type > lm_example = {"messages": [{"role": "user", "content": "What color is the sky?"}]} > apply_chat_template(lm_example, tokenizer) > # Output: {'text': '<|user|>\nWhat color is the sky?<|end|>\n<|endoftext|>'} > ``` > > - The prompt-only output includes a `'<|assistant|>\n'`, indicating the beginning of the assistant’s turn and expecting the model to generate a completion. > - In contrast, the language modeling output treats the input as a complete sequence and terminates it with `'<|endoftext|>'`, signaling the end of the text and not expecting any additional content. #### Prompt-completion A prompt-completion dataset includes a `"prompt"` and a `"completion"`. ```python # Standard format prompt_completion_example = {"prompt": "The sky is", "completion": " blue."} # Conversational format prompt_completion_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}]} ``` For examples of prompt-completion datasets, refer to the [Prompt-completion datasets collection](https://huggingface.co/collections/trl-lib/prompt-completion-datasets-677ea2bb20bbb6bdccada216). #### Preference A preference dataset is used for tasks where the model is trained to choose between two or more possible completions to the same prompt. This dataset includes a `"prompt"`, a `"chosen"` completion, and a `"rejected"` completion. The model is trained to select the `"chosen"` response over the `"rejected"` response. Some datasets may not include the `"prompt"` column, in which case the prompt is implicit and directly included in the `"chosen"` and `"rejected"` completions. We recommend using explicit prompts whenever possible. ```python # Standard format ## Explicit prompt (recommended) preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."} # Implicit prompt preference_example = {"chosen": "The sky is blue.", "rejected": "The sky is green."} # Conversational format ## Explicit prompt (recommended) preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}]} ## Implicit prompt preference_example = {"chosen": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}]} ``` For examples of preference datasets, refer to the [Preference datasets collection](https://huggingface.co/collections/trl-lib/preference-datasets-677e99b581018fcad9abd82c). Some preference datasets can be found with [the tag `dpo` on Hugging Face Hub](https://huggingface.co/datasets?other=dpo). You can also explore the [librarian-bots' DPO Collections](https://huggingface.co/collections/librarian-bots/direct-preference-optimization-datasets-66964b12835f46289b6ef2fc) to identify preference datasets. #### Unpaired preference An unpaired preference dataset is similar to a preference dataset but instead of having `"chosen"` and `"rejected"` completions for the same prompt, it includes a single `"completion"` and a `"label"` indicating whether the completion is preferred or not. ```python # Standard format unpaired_preference_example = {"prompt": "The sky is", "completion": " blue.", "label": True} # Conversational format unpaired_preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], "label": True} ``` For examples of unpaired preference datasets, refer to the [Unpaired preference datasets collection](https://huggingface.co/collections/trl-lib/unpaired-preference-datasets-677ea22bf5f528c125b0bcdf). #### Stepwise supervision A stepwise (or process) supervision dataset is similar to an [unpaired preference](#unpaired-preference) dataset but includes multiple steps of completions, each with its own label. This structure is useful for tasks that need detailed, step-by-step labeling, such as reasoning tasks. By evaluating each step separately and providing targeted labels, this approach helps identify precisely where the reasoning is correct and where errors occur, allowing for targeted feedback on each part of the reasoning process. ```python stepwise_example = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["The fractional part of 9.8 is 0.8, while the fractional part of 9.11 is 0.11.", "Since 0.11 is greater than 0.8, the number 9.11 is larger than 9.8."], "labels": [True, False] } ``` For examples of stepwise supervision datasets, refer to the [Stepwise supervision datasets collection](https://huggingface.co/collections/trl-lib/stepwise-supervision-datasets-677ea27fd4c5941beed7a96e). ## Which dataset type to use? Choosing the right dataset type depends on the task you are working on and the specific requirements of the TRL trainer you are using. Below is a brief overview of the dataset types supported by each TRL trainer. | Trainer | Expected dataset type | | --- | --- | | [`DPOTrainer`] | [Preference (explicit prompt recommended)](#preference) | | [`GRPOTrainer`] | [Prompt-only](#prompt-only) | | [`RewardTrainer`] | [Preference (implicit prompt recommended)](#preference) | | [`RLOOTrainer`] | [Prompt-only](#prompt-only) | | [`SFTTrainer`] | [Language modeling](#language-modeling) or [Prompt-completion](#prompt-completion) | | [`experimental.bco.BCOTrainer`] | [Unpaired preference](#unpaired-preference) or [Preference (explicit prompt recommended)](#preference) | | [`experimental.cpo.CPOTrainer`] | [Preference (explicit prompt recommended)](#preference) | | [`experimental.gkd.GKDTrainer`] | [Prompt-completion](#prompt-completion) | | [`experimental.kto.KTOTrainer`] | [Unpaired preference](#unpaired-preference) or [Preference (explicit prompt recommended)](#preference) | | [`experimental.nash_md.NashMDTrainer`] | [Prompt-only](#prompt-only) | | [`experimental.online_dpo.OnlineDPOTrainer`] | [Prompt-only](#prompt-only) | | [`experimental.orpo.ORPOTrainer`] | [Preference (explicit prompt recommended)](#preference) | | [`experimental.ppo.PPOTrainer`] | Tokenized language modeling | | [`experimental.prm.PRMTrainer`] | [Stepwise supervision](#stepwise-supervision) | | [`experimental.xpo.XPOTrainer`] | [Prompt-only](#prompt-only) | ## Using any dataset with TRL: preprocessing and conversion Many datasets come in formats tailored to specific tasks, which might not be directly compatible with TRL. To use such datasets with TRL, you may need to preprocess and convert them into the required format. To make this easier, we provide a set of [example scripts](https://github.com/huggingface/trl/tree/main/examples/datasets) that cover common dataset conversions. ### Example: UltraFeedback dataset Let’s take the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback) as an example. Here's a preview of the dataset: As shown above, the dataset format does not match the expected structure. It’s not in a conversational format, the column names differ, and the results pertain to different models (e.g., Bard, GPT-4) and aspects (e.g., "helpfulness", "honesty"). By using the provided conversion script [`examples/datasets/ultrafeedback.py`](https://github.com/huggingface/trl/tree/main/examples/datasets/ultrafeedback.py), you can transform this dataset into an unpaired preference type, and push it to the Hub: ```sh python examples/datasets/ultrafeedback.py --push_to_hub --repo_id trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness ``` Once converted, the dataset will look like this: Now, you can use this dataset with TRL! By adapting the provided scripts or creating your own, you can convert any dataset into a format compatible with TRL. ## Utilities for converting dataset types This section provides example code to help you convert between different dataset types. While some conversions can be performed after applying the chat template (i.e., in the standard format), we recommend performing the conversion before applying the chat template to ensure it works consistently. For simplicity, some of the examples below do not follow this recommendation and use the standard format. However, the conversions can be applied directly to the conversational format without modification. | From \ To | Language modeling | Prompt-completion | Prompt-only | Preference with implicit prompt | Preference | Unpaired preference | Stepwise supervision | | --- | --- | --- | --- | --- | --- | --- | --- | | Language modeling | N/A | N/A | N/A | N/A | N/A | N/A | N/A | | Prompt-completion | [🔗](#from-prompt-completion-to-language-modeling-dataset) | N/A | [🔗](#from-prompt-completion-to-prompt-only-dataset) | N/A | N/A | N/A | N/A | | Prompt-only | N/A | N/A | N/A | N/A | N/A | N/A | N/A | | Preference with implicit prompt | [🔗](#from-preference-with-implicit-prompt-to-language-modeling-dataset) | [🔗](#from-preference-with-implicit-prompt-to-prompt-completion-dataset) | [🔗](#from-preference-with-implicit-prompt-to-prompt-only-dataset) | N/A | [🔗](#from-implicit-to-explicit-prompt-preference-dataset) | [🔗](#from-preference-with-implicit-prompt-to-unpaired-preference-dataset) | N/A | | Preference | [🔗](#from-preference-to-language-modeling-dataset) | [🔗](#from-preference-to-prompt-completion-dataset) | [🔗](#from-preference-to-prompt-only-dataset) | [🔗](#from-explicit-to-implicit-prompt-preference-dataset) | N/A | [🔗](#from-preference-to-unpaired-preference-dataset) | N/A | | Unpaired preference | [🔗](#from-unpaired-preference-to-language-modeling-dataset) | [🔗](#from-unpaired-preference-to-prompt-completion-dataset) | [🔗](#from-unpaired-preference-to-prompt-only-dataset) | N/A | N/A | N/A | N/A | | Stepwise supervision | [🔗](#from-stepwise-supervision-to-language-modeling-dataset) | [🔗](#from-stepwise-supervision-to-prompt-completion-dataset) | [🔗](#from-stepwise-supervision-to-prompt-only-dataset) | N/A | N/A | [🔗](#from-stepwise-supervision-to-unpaired-preference-dataset) | N/A | ### From prompt-completion to language modeling dataset To convert a prompt-completion dataset into a language modeling dataset, concatenate the prompt and the completion. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is"], "completion": [" blue.", " in the sky."], }) def concat_prompt_completion(example): return {"text": example["prompt"] + example["completion"]} dataset = dataset.map(concat_prompt_completion, remove_columns=["prompt", "completion"]) ``` ```python >>> dataset[0] {'text': 'The sky is blue.'} ``` ### From prompt-completion to prompt-only dataset To convert a prompt-completion dataset into a prompt-only dataset, remove the completion. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is"], "completion": [" blue.", " in the sky."], }) dataset = dataset.remove_columns("completion") ``` ```python >>> dataset[0] {'prompt': 'The sky is'} ``` ### From preference with implicit prompt to language modeling dataset To convert a preference with implicit prompt dataset into a language modeling dataset, remove the rejected, and rename the column `"chosen"` to `"text"`. ```python from datasets import Dataset dataset = Dataset.from_dict({ "chosen": ["The sky is blue.", "The sun is in the sky."], "rejected": ["The sky is green.", "The sun is in the sea."], }) dataset = dataset.rename_column("chosen", "text").remove_columns("rejected") ``` ```python >>> dataset[0] {'text': 'The sky is blue.'} ``` ### From preference with implicit prompt to prompt-completion dataset To convert a preference dataset with implicit prompt into a prompt-completion dataset, extract the prompt with [`extract_prompt`], remove the rejected, and rename the column `"chosen"` to `"completion"`. ```python from datasets import Dataset from trl import extract_prompt dataset = Dataset.from_dict({ "chosen": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}], ], }) dataset = dataset.map(extract_prompt).remove_columns("rejected").rename_column("chosen", "completion") ``` ```python >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'completion': [{'role': 'assistant', 'content': 'It is blue.'}]} ``` ### From preference with implicit prompt to prompt-only dataset To convert a preference dataset with implicit prompt into a prompt-only dataset, extract the prompt with [`extract_prompt`], and remove the rejected and the chosen. ```python from datasets import Dataset from trl import extract_prompt dataset = Dataset.from_dict({ "chosen": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}], ], }) dataset = dataset.map(extract_prompt).remove_columns(["chosen", "rejected"]) ``` ```python >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}]} ``` ### From implicit to explicit prompt preference dataset To convert a preference dataset with implicit prompt into a preference dataset with explicit prompt, extract the prompt with [`extract_prompt`]. ```python from datasets import Dataset from trl import extract_prompt dataset = Dataset.from_dict({ "chosen": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}], ], }) dataset = dataset.map(extract_prompt) ``` ```python >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'chosen': [{'role': 'assistant', 'content': 'It is blue.'}], 'rejected': [{'role': 'assistant', 'content': 'It is green.'}]} ``` ### From preference with implicit prompt to unpaired preference dataset To convert a preference dataset with implicit prompt into an unpaired preference dataset, extract the prompt with [`extract_prompt`], and unpair the dataset with [`unpair_preference_dataset`]. ```python from datasets import Dataset from trl import extract_prompt, unpair_preference_dataset dataset = Dataset.from_dict({ "chosen": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}], [{"role": "user", "content": "Where is the sun?"}, {"role": "assistant", "content": "In the sea."}], ], }) dataset = dataset.map(extract_prompt) dataset = unpair_preference_dataset(dataset) ``` ```python >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'completion': [{'role': 'assistant', 'content': 'It is blue.'}], 'label': True} ``` > [!WARNING] > Keep in mind that the `"chosen"` and `"rejected"` completions in a preference dataset can be both good or bad. > Before applying [`unpair_preference_dataset`], please ensure that all `"chosen"` completions can be labeled as good and all `"rejected"` completions as bad. > This can be ensured by checking absolute rating of each completion, e.g. from a reward model. ### From preference to language modeling dataset To convert a preference dataset into a language modeling dataset, remove the rejected, concatenate the prompt and the chosen into the `"text"` column. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is"], "chosen": [" blue.", " in the sky."], "rejected": [" green.", " in the sea."], }) def concat_prompt_chosen(example): return {"text": example["prompt"] + example["chosen"]} dataset = dataset.map(concat_prompt_chosen, remove_columns=["prompt", "chosen", "rejected"]) ``` ```python >>> dataset[0] {'text': 'The sky is blue.'} ``` ### From preference to prompt-completion dataset To convert a preference dataset into a prompt-completion dataset, remove the rejected, and rename the column `"chosen"` to `"completion"`. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is"], "chosen": [" blue.", " in the sky."], "rejected": [" green.", " in the sea."], }) dataset = dataset.remove_columns("rejected").rename_column("chosen", "completion") ``` ```python >>> dataset[0] {'prompt': 'The sky is', 'completion': ' blue.'} ``` ### From preference to prompt-only dataset To convert a preference dataset into a prompt-only dataset, remove the rejected and the chosen. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is"], "chosen": [" blue.", " in the sky."], "rejected": [" green.", " in the sea."], }) dataset = dataset.remove_columns(["chosen", "rejected"]) ``` ```python >>> dataset[0] {'prompt': 'The sky is'} ``` ### From explicit to implicit prompt preference dataset To convert a preference dataset with explicit prompt into a preference dataset with implicit prompt, concatenate the prompt to both chosen and rejected, and remove the prompt. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What color is the sky?"}], [{"role": "user", "content": "Where is the sun?"}], ], "chosen": [ [{"role": "assistant", "content": "It is blue."}], [{"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "assistant", "content": "It is green."}], [{"role": "assistant", "content": "In the sea."}], ], }) def concat_prompt_to_completions(example): return {"chosen": example["prompt"] + example["chosen"], "rejected": example["prompt"] + example["rejected"]} dataset = dataset.map(concat_prompt_to_completions, remove_columns="prompt") ``` ```python >>> dataset[0] {'chosen': [{'role': 'user', 'content': 'What color is the sky?'}, {'role': 'assistant', 'content': 'It is blue.'}], 'rejected': [{'role': 'user', 'content': 'What color is the sky?'}, {'role': 'assistant', 'content': 'It is green.'}]} ``` ### From preference to unpaired preference dataset To convert dataset into an unpaired preference dataset, unpair the dataset with [`unpair_preference_dataset`]. ```python from datasets import Dataset from trl import unpair_preference_dataset dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What color is the sky?"}], [{"role": "user", "content": "Where is the sun?"}], ], "chosen": [ [{"role": "assistant", "content": "It is blue."}], [{"role": "assistant", "content": "In the sky."}], ], "rejected": [ [{"role": "assistant", "content": "It is green."}], [{"role": "assistant", "content": "In the sea."}], ], }) dataset = unpair_preference_dataset(dataset) ``` ```python >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'completion': [{'role': 'assistant', 'content': 'It is blue.'}], 'label': True} ``` > [!WARNING] > Keep in mind that the `"chosen"` and `"rejected"` completions in a preference dataset can be both good or bad. > Before applying [`unpair_preference_dataset`], please ensure that all `"chosen"` completions can be labeled as good and all `"rejected"` completions as bad. > This can be ensured by checking absolute rating of each completion, e.g. from a reward model. ### From unpaired preference to language modeling dataset To convert an unpaired preference dataset into a language modeling dataset, concatenate prompts with good completions into the `"text"` column, and remove the prompt, completion and label columns. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is", "The sky is", "The sun is"], "completion": [" blue.", " in the sky.", " green.", " in the sea."], "label": [True, True, False, False], }) def concatenate_prompt_completion(example): return {"text": example["prompt"] + example["completion"]} dataset = dataset.filter(lambda x: x["label"]).map(concatenate_prompt_completion).remove_columns(["prompt", "completion", "label"]) ``` ```python >>> dataset[0] {'text': 'The sky is blue.'} ``` ### From unpaired preference to prompt-completion dataset To convert an unpaired preference dataset into a prompt-completion dataset, filter for good labels, then remove the label columns. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is", "The sky is", "The sun is"], "completion": [" blue.", " in the sky.", " green.", " in the sea."], "label": [True, True, False, False], }) dataset = dataset.filter(lambda x: x["label"]).remove_columns(["label"]) ``` ```python >>> dataset[0] {'prompt': 'The sky is', 'completion': ' blue.'} ``` ### From unpaired preference to prompt-only dataset To convert an unpaired preference dataset into a prompt-only dataset, remove the completion and the label columns. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["The sky is", "The sun is", "The sky is", "The sun is"], "completion": [" blue.", " in the sky.", " green.", " in the sea."], "label": [True, True, False, False], }) dataset = dataset.remove_columns(["completion", "label"]) ``` ```python >>> dataset[0] {'prompt': 'The sky is'} ``` ### From stepwise supervision to language modeling dataset To convert a stepwise supervision dataset into a language modeling dataset, concatenate prompts with good completions into the `"text"` column. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["Blue light", "Water"], "completions": [[" scatters more in the atmosphere,", " so the sky is green."], [" forms a less dense structure in ice,", " which causes it to expand when it freezes."]], "labels": [[True, False], [True, True]], }) def concatenate_prompt_completions(example): completion = "".join(example["completions"]) return {"text": example["prompt"] + completion} dataset = dataset.filter(lambda x: all(x["labels"])).map(concatenate_prompt_completions, remove_columns=["prompt", "completions", "labels"]) ``` ```python >>> dataset[0] {'text': 'Blue light scatters more in the atmosphere, so the sky is green.'} ``` ### From stepwise supervision to prompt-completion dataset To convert a stepwise supervision dataset into a prompt-completion dataset, join the good completions and remove the labels. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["Blue light", "Water"], "completions": [[" scatters more in the atmosphere,", " so the sky is green."], [" forms a less dense structure in ice,", " which causes it to expand when it freezes."]], "labels": [[True, False], [True, True]], }) def join_completions(example): completion = "".join(example["completions"]) return {"completion": completion} dataset = dataset.filter(lambda x: all(x["labels"])).map(join_completions, remove_columns=["completions", "labels"]) ``` ```python >>> dataset[0] {'prompt': 'Blue light', 'completion': ' scatters more in the atmosphere, so the sky is green.'} ``` ### From stepwise supervision to prompt-only dataset To convert a stepwise supervision dataset into a prompt-only dataset, remove the completions and the labels. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["Blue light", "Water"], "completions": [[" scatters more in the atmosphere,", " so the sky is green."], [" forms a less dense structure in ice,", " which causes it to expand when it freezes."]], "labels": [[True, False], [True, True]], }) dataset = dataset.remove_columns(["completions", "labels"]) ``` ```python >>> dataset[0] {'prompt': 'Blue light'} ``` ### From stepwise supervision to unpaired preference dataset To convert a stepwise supervision dataset into an unpaired preference dataset, join the completions and merge the labels. The method for merging the labels depends on the specific task. In this example, we use the logical AND operation. This means that if the step labels indicate the correctness of individual steps, the resulting label will reflect the correctness of the entire sequence. ```python from datasets import Dataset dataset = Dataset.from_dict({ "prompt": ["Blue light", "Water"], "completions": [[" scatters more in the atmosphere,", " so the sky is green."], [" forms a less dense structure in ice,", " which causes it to expand when it freezes."]], "labels": [[True, False], [True, True]], }) def merge_completions_and_labels(example): return {"prompt": example["prompt"], "completion": "".join(example["completions"]), "label": all(example["labels"])} dataset = dataset.map(merge_completions_and_labels, remove_columns=["completions", "labels"]) ``` ```python >>> dataset[0] {'prompt': 'Blue light', 'completion': ' scatters more in the atmosphere, so the sky is green.', 'label': False} ``` ## Vision datasets Some trainers also support fine-tuning vision-language models (VLMs) using image-text pairs. In this scenario, it's recommended to use a conversational format, as each model handles image placeholders in text differently. A conversational vision dataset differs from a standard conversational dataset in two key ways: 1. The dataset must contain the key `images` with the image data (as lists of PIL images) or `image` with a single PIL image. 2. The `"content"` field in messages must be a list of dictionaries, where each dictionary specifies the type of data: `"image"` or `"text"`. Example: ```python # Textual dataset: "content": "What color is the sky?" # Vision dataset: "content": [ {"type": "image"}, {"type": "text", "text": "What color is the sky in the image?"} ] ``` An example of a conversational vision dataset is the [openbmb/RLAIF-V-Dataset](https://huggingface.co/datasets/openbmb/RLAIF-V-Dataset). Below is an embedded view of the dataset's training data, allowing you to explore it directly: > [!NOTE] > Mixing text-only and vision-language data in the dataset is possible, but it requires `transformers` version 4.57.0 or later. Example: > > ```python > dataset = Dataset.from_dict({ > "prompt": [ > [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What color is the sky in the image?"}]}], > [{"role": "user", "content": [{"type": "text", "text": "What is the capital of France?"}]}], > ], > "completion": [ > [{"role": "assistant", "content": [{"type": "text", "text": "It is blue."}]}], > [{"role": "assistant", "content": [{"type": "text", "text": "Paris."}]}], > ], > "images": [ > [PIL.Image.open("path/to/sky_image1.png")], > [], > ], > }) > ``` ================================================ FILE: docs/source/deepspeed_integration.md ================================================ # DeepSpeed Integration > [!WARNING] > Section under construction. Feel free to contribute! TRL supports training with DeepSpeed, a library that implements advanced training optimization techniques. These include optimizer state partitioning, offloading, gradient partitioning, and more. DeepSpeed integrates the [Zero Redundancy Optimizer (ZeRO)](https://huggingface.co/papers/1910.02054), which allows to scale the model size proportional to the number of devices with sustained high efficiency. ![ZeRO Stages](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/zero_stages.png) ## Installation To use DeepSpeed with TRL, install it using the following command: ```bash pip install deepspeed ``` ## Running Training Scripts with DeepSpeed No modifications to your training script are required. Simply run it with the DeepSpeed configuration file: ```bash accelerate launch --config_file train.py ``` We provide ready-to-use DeepSpeed configuration files in the [`examples/accelerate_configs`](https://github.com/huggingface/trl/tree/main/examples/accelerate_configs) directory. For example, to run training with ZeRO Stage 2, use the following command: ```bash accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml train.py ``` ## Additional Resources Consult the 🤗 Accelerate [documentation](https://huggingface.co/docs/accelerate/usage_guides/deepspeed) for more information about the DeepSpeed plugin. ================================================ FILE: docs/source/distributing_training.md ================================================ # Distributing Training > [!WARNING] > Section under construction. Feel free to contribute! ## Multi-GPU Training with TRL The trainers in TRL use [🤗 Accelerate](https://github.com/huggingface/accelerate) to enable distributed training across multiple GPUs or nodes. To do so, first create an [🤗 Accelerate](https://github.com/huggingface/accelerate) config file by running ```bash accelerate config ``` and answering the questions according to your multi-GPU / multi-node setup. You can then launch distributed training by running: ```bash accelerate launch train.py ``` We also provide config files in the [examples folder](https://github.com/huggingface/trl/tree/main/examples/accelerate_configs) that can be used as templates. To use these templates, simply pass the path to the config file when launching a job, e.g.: ```shell accelerate launch --config_file examples/accelerate_configs/multi_gpu.yaml train.py ``` This automatically distributes the workload across all available GPUs. Under the hood, [🤗 Accelerate](https://github.com/huggingface/accelerate) creates one model per GPU. Each process: - Processes its own batch of data - Computes the loss and gradients for that batch - Shares gradient updates across all GPUs ![multi gpu](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/multi_gpu.png) The effective batch size is calculated as: $$ \text{Batch Size} = \text{per\_device\_train\_batch\_size} \times \text{num\_devices} \times \text{gradient\_accumulation\_steps} $$ To maintain a consistent batch size when scaling to multiple GPUs, make sure to update `per_device_train_batch_size` and `gradient_accumulation_steps` accordingly. Example, these configurations are equivalent, and should yield the same results: | Number of GPUs | Per device batch size | Gradient accumulation steps | Comments | | --- | --- | --- | --- | | 1 | 32 | 1 | Possibly high memory usage, but faster training | | 1 | 4 | 8 | Lower memory usage, slower training | | 8 | 4 | 1 | Multi-GPU to get the best of both worlds | > [!TIP] > Having one model per GPU can lead to high memory usage, which may not be feasible for large models or low-memory GPUs. In such cases, you can leverage [DeepSpeed](https://github.com/deepspeedai/DeepSpeed), which provides optimizations like model sharding, Zero Redundancy Optimizer, mixed precision training, and offloading to CPU or NVMe. Check out our [DeepSpeed Integration](deepspeed_integration) guide for more details. ## Sequence Parallelism for Long Context Training Sequence Parallelism (also called Context Parallelism) is a parallelization technique that enables training with longer sequences by splitting the sequence dimension across multiple GPUs. Each GPU processes a portion of the sequence, allowing you to train with sequences longer than what would fit on a single GPU's memory. > [!NOTE] > **Terminology clarification:** This section describes parallelism techniques for splitting sequences to enable longer context training: > - **Context Parallelism (CP)**: Splits sequences across GPUs (implemented as Ring Attention with FSDP2) > - **Sequence Parallelism (SP)**: Another form of sequence splitting (implemented as ALST/Ulysses with DeepSpeed) > > Both CP and SP are different from traditional Sequence Parallelism used with Tensor Parallelism (TP+SP) to reduce activation memory. With the techniques here, parallelism dimensions multiply: `TP=2` and `CP=2` would require 4 GPUs (2×2), whereas traditional `TP+SP=2` only needs 2 GPUs as they share the same ranks. > > In Accelerate's `ParallelismConfig`: > - Use `cp_size` with `cp_backend="torch"` for Ring Attention (FSDP2) > - Use `sp_size` with `sp_backend="deepspeed"` for ALST/Ulysses (DeepSpeed) Sequence parallelism is particularly useful when: - You want to train with very long sequences (>32k tokens) - Single GPU memory is insufficient for your desired sequence length - You need to maintain sequence coherence across the full context ### Available Implementations TRL supports two sequence parallelism implementations, each with different characteristics: 1. **Ring Attention (FSDP2)** - Uses ring-based communication for memory-efficient processing of extremely long sequences 2. **ALST/Ulysses (DeepSpeed)** - Uses attention head parallelism for faster training with high-bandwidth interconnects > [!IMPORTANT] > **Sequence Length Terminology:** When using Context Parallelism, the sequence is split across GPUs, introducing two concepts: > - **Global sequence length**: The full sequence length before splitting across GPUs > - **Micro sequence length**: The sequence length per GPU after splitting > > In TRL, `max_seq_length` (or `max_length`) refers to the **global sequence length**. The framework automatically handles splitting into micro sequences: > - **Ring Attention (FSDP2)**: Uses `cp_size` to split sequences. With `max_seq_length=8192` and `cp_size=4`, each GPU processes 2048 tokens. > - **ALST/Ulysses (DeepSpeed)**: Uses `sp_size` (with `sp_backend="deepspeed"`) to split sequences. With `max_seq_length=8192` and `sp_size=2`, each GPU processes 4096 tokens. > > The Trainer automatically accounts for context parallelism when calculating batch sizes and training metrics. ### Choosing Between Ring Attention and Ulysses The comparison table below highlights the key differences between the two approaches: | Feature | Ring Attention (FSDP2) | ALST/Ulysses (DeepSpeed) | |---------|----------|-------------------------| | **Method** | Ring Self-Attention | Attention Head Parallelism | | **Backend** | PyTorch FSDP2 | DeepSpeed ZeRO | | **Attention** | SDPA only | Flash Attention 2 or SDPA | | **Minimum Accelerate** | 1.11.0+ | 1.12.0+ | | **Minimum DeepSpeed** | N/A | 0.18.1+ | | **Sequence Divisibility** | `cp_size * 2` | `sp_size` | | **Zero Stage** | N/A | ZeRO Stage 1/2/3 | **Ring Attention is better when:** - You need to handle extremely long sequences (1M+ tokens) - The model has limited attention heads (Ring Attention is not constrained by head count) - You want flexibility in scaling to any sequence length - Network topology is limited (Ring Attention works with simple P2P ring communication) **Ulysses is better when:** - You have high-bandwidth, low-latency interconnects (NVLink, InfiniBand) - The model has many attention heads that can be split across GPUs - You want lower communication volume - You want faster training speed for moderate sequence lengths (up to ~500k tokens) **Key Trade-offs:** - **Communication Volume:** Ulysses has lower communication volume, making it more efficient with good interconnects. Ring Attention has higher communication volume but is more flexible with different network topologies. - **Attention Head Constraints:** Ulysses is limited by the number of attention heads (requires `num_heads >= sp_size`). Ring Attention scales with sequence length regardless of model architecture. - **Network Sensitivity:** Ulysses all-to-all communication is sensitive to network latency. Ring Attention uses P2P ring communication which is more tolerant of varying network conditions. For a detailed comparison, see the [Ulysses and Ring Attention blog post](https://huggingface.co/blog/exploding-gradients/ulysses-ring-attention). ### Ring Attention Implementation (FSDP2) Ring Attention uses a ring-like communication pattern where each GPU processes a portion of the sequence and passes information to the next GPU in the ring. #### Requirements and Limitations 1. **Accelerate 1.11.0 or higher** is required for Ring Attention / Context Parallelism support 2. **FSDP2 (PyTorch FSDP v2)** is required as the distributed training backend 3. **SDPA attention** - Flash Attention is currently not supported 4. **Sequence length divisibility** - sequences must be divisible by `cp_size * 2`. This is automatically handled using the `pad_to_multiple_of` parameter in the data collator. #### Configuration ##### Accelerate Configuration Use one of the provided accelerate config files (e.g. [`context_parallel_2gpu.yaml`](https://github.com/huggingface/trl/blob/main/examples/accelerate_configs/context_parallel_2gpu.yaml) for 2 GPUs): ```yaml compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: true # Enable activation checkpointing for memory efficiency fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_cpu_ram_efficient_loading: true fsdp_offload_params: false fsdp_reshard_after_forward: true fsdp_state_dict_type: FULL_STATE_DICT fsdp_version: 2 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 2 # Number of GPUs rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false parallelism_config: parallelism_config_dp_replicate_size: 1 parallelism_config_dp_shard_size: 1 parallelism_config_tp_size: 1 parallelism_config_cp_size: 2 # Context parallel size ``` ##### Training Configuration ```python from trl import SFTConfig training_args = SFTConfig( # required pad_to_multiple_of=4, # ensures divisibility by cp_size * 2 # to get the most out of CP max_length=16384, # long sequence length packing=True, # use packing to reduce padding use_liger_kernel=True, # compatible with CP gradient_checkpointing=False, # The activation_checkpointing in FSDP config and the gradient_checkpointing in training arg can't be set to True simultaneously per_device_train_batch_size=1, ... ) ``` Then, launch your training script with the appropriate accelerate config file: ```bash accelerate launch --config_file context_parallel_2gpu.yaml train.py ``` #### Best Practices 1. **Use the `pad_to_multiple_of` parameter** - This is now the recommended way to ensure sequence length divisibility: - For `cp_size=2`: use `pad_to_multiple_of=4` (since `cp_size * 2 = 4`) - For `cp_size=4`: use `pad_to_multiple_of=8` (since `cp_size * 2 = 8`) - The data collator automatically pads sequences to the required multiple, ensuring compatibility with CP 2. **Use packing with padding** - The default BFD (Best Fit Decreasing) strategy works perfectly: - Preserves sequence boundaries and maintains training quality - Works seamlessly with both `padding_free=True` and standard padding modes 3. **Combine with other memory optimizations** like Liger kernels, bfloat16, and gradient checkpointing 4. **Start with smaller context parallel sizes** (2-4 GPUs) before scaling up 5. **Monitor memory usage** across all GPUs to ensure balanced workload #### Benchmarking Ring Attention We benchmarked Ring Attention to highlight its potential improvements in training efficiency. Our experiments were conducted using **1, 2, 4, and 8 H100 GPUs**, though the results can be extended to larger clusters with more nodes and GPUs. For the setup, we fine-tuned an **8B model** ([Qwen/Qwen3-8B](https://huggingface.co/Qwen/Qwen3-8B)) using the provided accelerate configuration ([`context_parallel_2gpu.yaml`](https://github.com/huggingface/trl/blob/main/examples/accelerate_configs/context_parallel_2gpu.yaml)). We adjusted `num_processes` and `parallelism_config_cp_size` based on the number of GPUs for each run. Training was performed with the [sft.py](https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py) example script, combined with the parameters described above. The results below summarize the **maximum trainable sequence length** and **iterations per second** for different numbers of GPUs. A value marked as `OOM` indicates that the configuration ran out of memory and could not be trained. These results show that **Context Parallelism (CP) scales effectively with more GPUs**, enabling training on much longer sequences. With **8 GPUs**, context lengths of over **300k tokens** become feasible, unlocking training with extremely long contexts while maintaining reasonable throughput.
CP Max content length CP seconds/iteration
> [!TIP] > Accelerate also supports **N-Dimensional Parallelism (ND-parallelism)**, which enables you to combine different parallelization strategies to efficiently distribute model training across multiple GPUs. > > You can learn more and explore configuration examples in the [Accelerate ND-parallelism guide](https://github.com/huggingface/accelerate/blob/main/examples/torch_native_parallelism/README.md#nd-parallelism). ### ALST/Ulysses Implementation (DeepSpeed) ALST (Arctic Long Sequence Training) / Ulysses uses attention head parallelism to split long sequences across GPUs, working with DeepSpeed's ZeRO optimizer. > [!NOTE] > **Technical Note on Parallelism Configuration:** > - **DeepSpeed ALST/Ulysses** uses `sp_size` with `sp_backend="deepspeed"` in both YAML and Python API > - **Ring Attention (FSDP2)** uses `cp_size` with `cp_backend="torch"` > > The Trainer automatically accounts for both CP and SP when calculating effective batch sizes and training metrics. #### Requirements and Limitations 1. **DeepSpeed 0.18.1 or higher** is required 2. **Accelerate 1.12.0 or higher** is required for ALST/Ulysses sequence parallelism support 3. **Attention implementation** - Flash Attention 2 recommended (clean output), SDPA works as fallback 4. **Sequence length divisibility** - sequences must be divisible by `sp_size`. Use `pad_to_multiple_of` in your training config. 5. **Parallelism configuration** - You must ensure `dp_replicate_size × dp_shard_size × sp_size = num_processes` #### Configuration ##### Accelerate Configuration Use the provided accelerate config file ([`alst_ulysses_4gpu.yaml`](https://github.com/huggingface/trl/blob/main/examples/accelerate_configs/alst_ulysses_4gpu.yaml)): ```yaml compute_environment: LOCAL_MACHINE debug: false deepspeed_config: zero_stage: 3 seq_parallel_communication_data_type: bf16 distributed_type: DEEPSPEED mixed_precision: bf16 num_machines: 1 num_processes: 4 # Number of GPUs parallelism_config: parallelism_config_dp_replicate_size: 1 parallelism_config_dp_shard_size: 2 # Enables 2D parallelism with SP parallelism_config_tp_size: 1 parallelism_config_sp_size: 2 # Sequence parallel size parallelism_config_sp_backend: deepspeed parallelism_config_sp_seq_length_is_variable: true parallelism_config_sp_attn_implementation: flash_attention_2 ``` ##### Training Configuration ```python from trl import SFTConfig training_args = SFTConfig( # required pad_to_multiple_of=2, # Must equal sp_size # to get the most out of SP max_seq_length=4096, packing=True, attn_implementation="flash_attention_2", per_device_train_batch_size=1, ... ) ``` Then, launch your training script with the appropriate accelerate config file: ```bash accelerate launch --config_file examples/accelerate_configs/alst_ulysses_4gpu.yaml train.py ``` #### 2D Parallelism The 4 GPU configuration above automatically enables 2D parallelism by combining Data Parallelism (DP) with Sequence Parallelism (SP). With `sp_size=2` and `dp_shard_size=2`, the 4 GPUs are organized as: - 2 sequence parallel groups (processing the same data split across sequences) - 2 data parallel groups (processing different data) To adjust the parallelism for different GPU counts, modify the YAML config: | GPUs | sp_size | dp_shard_size | Use Case | YAML Changes | |------|---------|---------------|----------|--------------| | 4 | 2 | 2 | Balanced - longer sequences + more data | `num_processes: 4`, `sp_size: 2`, `dp_shard_size: 2` | | 4 | 4 | 1 | Pure SP for maximum sequence length | `num_processes: 4`, `sp_size: 4`, `dp_shard_size: 1` | | 8 | 2 | 4 | Large-scale training | `num_processes: 8`, `sp_size: 2`, `dp_shard_size: 4` | #### Best Practices 1. **Use `pad_to_multiple_of`** to ensure sequences are divisible by `sp_size` 2. **Use Flash Attention 2** for clean output (SDPA works but shows packing warnings) 3. **Start with `sp_size=2`** before scaling to larger values 4. **Use DeepSpeed ZeRO Stage 3** for large models 5. **Combine with memory optimizations** like Liger kernels and gradient checkpointing 6. **Validate parallelism config**: Ensure `dp_replicate_size × dp_shard_size × sp_size = num_processes` #### Complete Example Here's how to run ALST/Ulysses training using the built-in [`sft.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py) script with 4 GPUs: ```bash accelerate launch --config_file examples/accelerate_configs/alst_ulysses_4gpu.yaml \ trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2e-4 \ --max_steps 100 \ --max_seq_length 4096 \ --packing \ --packing_strategy wrapped \ --torch_dtype bfloat16 \ --attn_implementation flash_attention_2 \ --output_dir output-alst-4gpu \ --logging_steps 10 \ --report_to trackio ``` This command automatically: - Configures 2D parallelism (SP=2, DP=2) across 4 GPUs - Uses Flash Attention 2 for clean training - Enables packing with automatic padding to ensure sequence divisibility - Leverages DeepSpeed ZeRO Stage 3 for memory efficiency ### Further Reading #### General Resources - [Hugging Face Blog: Understanding Ulysses and Ring Attention](https://huggingface.co/blog/exploding-gradients/ulysses-ring-attention) - Detailed comparison of Ring Attention vs Ulysses approaches - [Accelerate: Context Parallelism Guide](https://huggingface.co/docs/accelerate/concept_guides/context_parallelism) - [Hugging Face Blog: Enabling Long-Context Training with Sequence Parallelism in Axolotl](https://huggingface.co/blog/axolotl-ai-co/long-context-with-sequence-parallelism-in-axolotl) #### Ring Attention (FSDP2) - [Ultrascale Playbook - Context Parallelism](https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=context_parallelism) - [Accelerate Example: 128k Sequence Length](https://github.com/huggingface/accelerate/blob/main/examples/torch_native_parallelism/README.md#context-parallelism-128k-sequence-length) - [Accelerate ND-parallelism Guide](https://github.com/huggingface/accelerate/blob/main/examples/torch_native_parallelism/README.md#nd-parallelism) #### ALST/Ulysses (DeepSpeed) - [DeepSpeed Sequence Parallelism Documentation](https://www.deepspeed.ai/tutorials/ds-sequence/) - [Snowflake Engineering Blog: Arctic Long Sequence Training (ALST)](https://www.snowflake.com/en/engineering-blog/arctic-long-sequence-training-multi-million-token-ai/) ## Multi-Node Training When a single machine doesn't have enough GPUs, TRL can scale training across multiple machines (nodes) using [🤗 Accelerate](https://huggingface.co/docs/accelerate/basic_tutorials/launch#multi-node-training). ### Accelerate Configuration Create an `accelerate` config file (e.g., `multi_node.yaml`) for multi-node training. Key fields: ```yaml compute_environment: LOCAL_MACHINE distributed_type: MULTI_GPU num_machines: 2 machine_rank: 0 # 0 for main node, 1 for second node main_process_ip: 10.0.0.1 # IP of rank 0 node main_process_port: 29500 num_processes: 16 # total processes across nodes mixed_precision: bf16 use_cpu: false same_network: true ``` Adjust `num_processes` to match the total number of GPUs across all nodes. > [!NOTE] > Replace `10.0.0.1` with the actual IP address of the rank 0 (main) node. ### Launching #### Option 1: Manual Launch (Non-HPC) Run the following on each node manually: ```bash # Node 0 (main node) accelerate launch --config_file multi_node.yaml --machine_rank 0 train.py # Node 1 accelerate launch --config_file multi_node.yaml --machine_rank 1 train.py ``` #### Option 2: SLURM Launch (HPC Clusters) For clusters using SLURM job scheduler, create a job script (e.g., `slurm_job.sh`): ```bash #!/bin/bash #SBATCH --nodes=2 #SBATCH --gpus-per-node=8 #SBATCH --job-name=trl_multi srun accelerate launch --config_file multi_node.yaml train.py ``` Then submit the job: ```bash sbatch slurm_job.sh ``` SLURM automatically distributes the training across all requested nodes and GPUs, and `srun` configures the necessary environment variables for multi-node communication. **Key SLURM directives:** - `--nodes=2`: Request 2 compute nodes - `--gpus-per-node=8`: Allocate 8 GPUs per node (16 total) - `--job-name`: Label for tracking in the job queue You can combine multi-node with DeepSpeed by setting `distributed_type: DEEPSPEED` and adding a `deepspeed_config` block. See the [DeepSpeed integration guide](https://huggingface.co/docs/trl/en/deepspeed_integration). ### Further Reading - [Accelerate: Launching Scripts](https://huggingface.co/docs/accelerate/basic_tutorials/launch) - [Accelerate: Example Zoo](https://huggingface.co/docs/accelerate/usage_guides/training_zoo) - [SLURM Workload Manager Documentation](https://slurm.schedmd.com/) - For cluster job scheduling ================================================ FILE: docs/source/dpo_trainer.md ================================================ # DPO Trainer [![All_models-DPO-blue](https://img.shields.io/badge/All_models-DPO-blue)](https://huggingface.co/models?other=dpo,trl) [![smol_course-Chapter_2-yellow](https://img.shields.io/badge/smol_course-Chapter_2-yellow)](https://github.com/huggingface/smol-course/tree/main/2_preference_alignment) ## Overview TRL supports the Direct Preference Optimization (DPO) Trainer for training language models, as described in the paper [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](https://huggingface.co/papers/2305.18290) by [Rafael Rafailov](https://huggingface.co/rmrafailov), Archit Sharma, Eric Mitchell, [Stefano Ermon](https://huggingface.co/ermonste), [Christopher D. Manning](https://huggingface.co/manning), [Chelsea Finn](https://huggingface.co/cbfinn). The abstract from the paper is the following: > While large-scale unsupervised language models (LMs) learn broad world knowledge and some reasoning skills, achieving precise control of their behavior is difficult due to the completely unsupervised nature of their training. Existing methods for gaining such steerability collect human labels of the relative quality of model generations and fine-tune the unsupervised LM to align with these preferences, often with reinforcement learning from human feedback (RLHF). However, RLHF is a complex and often unstable procedure, first fitting a reward model that reflects the human preferences, and then fine-tuning the large unsupervised LM using reinforcement learning to maximize this estimated reward without drifting too far from the original model. In this paper we introduce a new parameterization of the reward model in RLHF that enables extraction of the corresponding optimal policy in closed form, allowing us to solve the standard RLHF problem with only a simple classification loss. The resulting algorithm, which we call Direct Preference Optimization (DPO), is stable, performant, and computationally lightweight, eliminating the need for sampling from the LM during fine-tuning or performing significant hyperparameter tuning. Our experiments show that DPO can fine-tune LMs to align with human preferences as well as or better than existing methods. Notably, fine-tuning with DPO exceeds PPO-based RLHF in ability to control sentiment of generations, and matches or improves response quality in summarization and single-turn dialogue while being substantially simpler to implement and train. This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif) and later refactored by [Quentin Gallouédec](https://huggingface.co/qgallouedec). ## Quick start This example demonstrates how to train a language model using the [`DPOTrainer`] from TRL. We train a [Qwen 3 0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) model on the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). ```python from trl import DPOTrainer from datasets import load_dataset trainer = DPOTrainer( model="Qwen/Qwen3-0.6B", train_dataset=load_dataset("trl-lib/ultrafeedback_binarized", split="train"), ) trainer.train() ``` ## Expected dataset type and format DPO requires a [preference](dataset_formats#preference) dataset. The [`DPOTrainer`] is compatible with both [standard](dataset_formats#standard) and [conversational](dataset_formats#conversational) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ```python # Standard format ## Explicit prompt (recommended) preference_example = {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."} # Implicit prompt preference_example = {"chosen": "The sky is blue.", "rejected": "The sky is green."} # Conversational format ## Explicit prompt (recommended) preference_example = {"prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}]} ## Implicit prompt preference_example = {"chosen": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}]} ``` If your dataset is not in one of these formats, you can preprocess it to convert it into the expected format. Here is an example with the [Vezora/Code-Preference-Pairs](https://huggingface.co/datasets/Vezora/Code-Preference-Pairs) dataset: ```python from datasets import load_dataset dataset = load_dataset("Vezora/Code-Preference-Pairs") def preprocess_function(example): return { "prompt": [{"role": "user", "content": example["input"]}], "chosen": [{"role": "assistant", "content": example["accepted"]}], "rejected": [{"role": "assistant", "content": example["rejected"]}], } dataset = dataset.map(preprocess_function, remove_columns=["instruction", "input", "accepted", "ID"]) print(next(iter(dataset["train"]))) ``` ```json { "prompt": [{"role": "user", "content": "Create a nested loop to print every combination of numbers [...]"}], "chosen": [{"role": "assistant", "content": "Here is an example of a nested loop in Python [...]"}], "rejected": [{"role": "assistant", "content": "Here is an example of a nested loop in Python [...]"}], } ``` ## Looking deeper into the DPO method Direct Preference Optimization (DPO) is a training method designed to align a language model with preference data. Instead of supervised input–output pairs, the model is trained on pairs of completions to the same prompt, where one completion is preferred over the other. The objective directly optimizes the model to widen the margin between the log-likelihoods of preferred and dispreferred completions, relative to a reference model, without requiring an explicit reward model. In practice, this is typically achieved by suppressing the likelihood of dispreferred completions rather than by increasing the likelihood of preferred ones. This section breaks down how DPO works in practice, covering the key steps: **preprocessing** and **loss computation**. ### Preprocessing and tokenization During training, each example is expected to contain a prompt along with a preferred (`chosen`) and a dispreferred (`rejected`) completion. For more details on the expected formats, see [Dataset formats](dataset_formats). The [`DPOTrainer`] tokenizes each input using the model's tokenizer. ### Computing the loss ![dpo_figure](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/dpo_figure.png) The loss used in DPO is defined as follows: $$ \mathcal{L}_{\mathrm{DPO}}(\theta) = -\mathbb{E}_{(x,y^{+},y^{-})}\!\left[\log \sigma\!\left(\beta\Big(\log\frac{\pi_{\theta}(y^{+}\!\mid x)}{\pi_{\mathrm{ref}}(y^{+}\!\mid x)}-\log \frac{\pi_{\theta}(y^{-}\!\mid x)}{\pi_{\mathrm{ref}}(y^{-}\!\mid x)}\Big)\right)\right] $$ where \\( x \\) is the prompt, \\( y^+ \\) is the preferred completion and \\( y^- \\) is the dispreferred completion. \\( \pi_{\theta} \\) is the policy model being trained, \\( \pi_{\mathrm{ref}} \\) is the reference model, \\( \sigma \\) is the sigmoid function, and \\( \beta > 0 \\) is a hyperparameter that controls the strength of the preference signal. #### Loss Types Several formulations of the objective have been proposed in the literature. Initially, the objective of DPO was defined as presented above. | `loss_type=` | Description | | --- | --- | | `"sigmoid"` (default) | Given the preference data, we can fit a binary classifier according to the Bradley-Terry model and in fact the [DPO](https://huggingface.co/papers/2305.18290) authors propose the sigmoid loss on the normalized likelihood via the `logsigmoid` to fit a logistic regression. | | `"hinge"` | The [RSO](https://huggingface.co/papers/2309.06657) authors propose to use a hinge loss on the normalized likelihood from the [SLiC](https://huggingface.co/papers/2305.10425) paper. In this case, the `beta` is the reciprocal of the margin. | | `"ipo"` | The [IPO](https://huggingface.co/papers/2310.12036) authors argue the logit transform can overfit and propose the identity transform to optimize preferences directly; TRL exposes this as `loss_type="ipo"`. | | `"exo_pair"` | The [EXO](https://huggingface.co/papers/2402.00856) authors propose reverse-KL preference optimization. `label_smoothing` must be strictly greater than `0.0`; a recommended value is `1e-3` (see Eq. 16 for the simplified pairwise variant). The full method uses `K>2` SFT completions and approaches PPO as `K` grows. | | `"nca_pair"` | The [NCA](https://huggingface.co/papers/2402.05369) authors shows that NCA optimizes the absolute likelihood for each response rather than the relative likelihood. | | `"robust"` | The [Robust DPO](https://huggingface.co/papers/2403.00409) authors propose an unbiased DPO loss under noisy preferences. Use `label_smoothing` in [`DPOConfig`] to model label-flip probability; valid values are in the range `[0.0, 0.5)`. | | `"bco_pair"` | The [BCO](https://huggingface.co/papers/2404.04656) authors train a binary classifier whose logit serves as a reward so that the classifier maps {prompt, chosen completion} pairs to 1 and {prompt, rejected completion} pairs to 0. For unpaired data, we recommend the dedicated [`experimental.bco.BCOTrainer`]. | | `"sppo_hard"` | The [SPPO](https://huggingface.co/papers/2405.00675) authors claim that SPPO is capable of solving the Nash equilibrium iteratively by pushing the chosen rewards to be as large as 1/2 and the rejected rewards to be as small as -1/2 and can alleviate data sparsity issues. The implementation approximates this algorithm by employing hard label probabilities, assigning 1 to the winner and 0 to the loser. | | `"aot"` or `loss_type="aot_unpaired"` | The [AOT](https://huggingface.co/papers/2406.05882) authors propose Distributional Preference Alignment via Optimal Transport. `loss_type="aot"` is for paired data; `loss_type="aot_unpaired"` is for unpaired data. Both enforce stochastic dominance via sorted quantiles; larger per-GPU batch sizes help. | | `"apo_zero"` or `loss_type="apo_down"` | The [APO](https://huggingface.co/papers/2408.06266) method introduces an anchored objective. `apo_zero` boosts winners and downweights losers (useful when the model underperforms the winners). `apo_down` downweights both, with stronger pressure on losers (useful when the model already outperforms winners). | | `"discopop"` | The [DiscoPOP](https://huggingface.co/papers/2406.08414) paper uses LLMs to discover more efficient offline preference optimization losses. In the paper the proposed DiscoPOP loss (which is a log-ratio modulated loss) outperformed other optimization losses on different tasks (IMDb positive text generation, Reddit TLDR summarization, and Alpaca Eval 2.0). | | `"sft"` | SFT (Supervised Fine-Tuning) loss is the negative log likelihood loss, used to train the model to generate preferred responses. | ## Logged metrics While training and evaluating we record the following reward metrics: * `global_step`: The total number of optimizer steps taken so far. * `epoch`: The current epoch number, based on dataset iteration. * `num_tokens`: The total number of tokens processed so far. * `loss`: The average cross-entropy loss computed over non-masked tokens in the current logging interval. * `entropy`: The average entropy of the model's predicted token distribution over non-masked tokens. * `mean_token_accuracy`: The proportion of non-masked tokens for which the model’s top-1 prediction matches the token from the chosen completion. * `learning_rate`: The current learning rate, which may change dynamically if a scheduler is used. * `grad_norm`: The L2 norm of the gradients, computed before gradient clipping. * `logits/chosen`: The average logit values assigned by the model to the tokens in the chosen completion. * `logits/rejected`: The average logit values assigned by the model to the tokens in the rejected completion. * `logps/chosen`: The average log-probability assigned by the model to the tokens in the chosen completion. * `logps/rejected`: The average log-probability assigned by the model to the tokens in the rejected completion. * `rewards/chosen`: The average implicit reward computed for the chosen completion, computed as \\( \beta \log \frac{\pi_{\theta}(y^{+}\!\mid x)}{\pi_{\mathrm{ref}}(y^{+}\!\mid x)} \\). * `rewards/rejected`: The average implicit reward computed for the rejected completion, computed as \\( \beta \log \frac{\pi_{\theta}(y^{-}\!\mid x)}{\pi_{\mathrm{ref}}(y^{-}\!\mid x)} \\). * `rewards/margins`: The average implicit reward margin between the chosen and rejected completions. * `rewards/accuracies`: The proportion of examples where the implicit reward for the chosen completion is higher than that for the rejected completion. ## Customization ### Compatibility and constraints Some argument combinations are intentionally restricted in the current [`DPOTrainer`] implementation: * `use_weighting=True` is not supported with `loss_type="aot"` or `loss_type="aot_unpaired"`. * With `use_liger_kernel=True`: * only a single `loss_type` is supported, * `compute_metrics` is not supported, * `precompute_ref_log_probs=True` is not supported. * `sync_ref_model=True` is not supported when training with PEFT models that do not keep a standalone `ref_model`. * `sync_ref_model=True` cannot be combined with `precompute_ref_log_probs=True`. * `precompute_ref_log_probs=True` is not supported with `IterableDataset` (train or eval). ### Multi-loss combinations The DPO trainer supports combining multiple loss functions with different weights, enabling more sophisticated optimization strategies. This is particularly useful for implementing algorithms like MPO (Mixed Preference Optimization). MPO is a training approach that combines multiple optimization objectives, as described in the paper [Enhancing the Reasoning Ability of Multimodal Large Language Models via Mixed Preference Optimization](https://huggingface.co/papers/2411.10442). To combine multiple losses, specify the loss types and corresponding weights as lists: ```python # MPO: Combines DPO (sigmoid) for preference and BCO (bco_pair) for quality training_args = DPOConfig( loss_type=["sigmoid", "bco_pair", "sft"], # loss types to combine loss_weights=[0.8, 0.2, 1.0] # corresponding weights, as used in the MPO paper ) ``` ### Model initialization You can directly pass the kwargs of the [`~transformers.AutoModelForCausalLM.from_pretrained()`] method to the [`DPOConfig`]. For example, if you want to load a model in a different precision, analogous to ```python model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B", dtype=torch.bfloat16) ``` you can do so by passing the `model_init_kwargs={"dtype": torch.bfloat16}` argument to the [`DPOConfig`]. ```python from trl import DPOConfig training_args = DPOConfig( model_init_kwargs={"dtype": torch.bfloat16}, ) ``` Note that all keyword arguments of [`~transformers.AutoModelForCausalLM.from_pretrained()`] are supported. ### Train adapters with PEFT We support tight integration with 🤗 PEFT library, allowing any user to conveniently train adapters and share them on the Hub, rather than training the entire model. ```python from datasets import load_dataset from trl import DPOTrainer from peft import LoraConfig dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = DPOTrainer( "Qwen/Qwen3-0.6B", train_dataset=dataset, peft_config=LoraConfig(), ) trainer.train() ``` You can also continue training your [`~peft.PeftModel`]. For that, first load a `PeftModel` outside [`DPOTrainer`] and pass it directly to the trainer without the `peft_config` argument being passed. ```python from datasets import load_dataset from trl import DPOTrainer from peft import AutoPeftModelForCausalLM model = AutoPeftModelForCausalLM.from_pretrained("trl-lib/Qwen3-4B-LoRA", is_trainable=True) dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = DPOTrainer( model=model, train_dataset=dataset, ) trainer.train() ``` > [!TIP] > When training adapters, you typically use a higher learning rate (≈1e‑5) than full fine-tuning since only new parameters are being learned. > > ```python > DPOConfig(learning_rate=1e-5, ...) > ``` ### Train with Liger Kernel Liger Kernel is a collection of Triton kernels for LLM training that boosts multi-GPU throughput by 20%, cuts memory use by 60% (enabling up to 4× longer context), and works seamlessly with tools like FlashAttention, PyTorch FSDP, and DeepSpeed. For more information, see [Liger Kernel Integration](liger_kernel_integration). ### Rapid Experimentation for DPO RapidFire AI is an open-source experimentation engine that sits on top of TRL and lets you launch multiple DPO configurations at once, even on a single GPU. Instead of trying configurations sequentially, RapidFire lets you **see all their learning curves earlier, stop underperforming runs, and clone promising ones with new settings in flight** without restarting. For more information, see [RapidFire AI Integration](rapidfire_integration). ### Train with Unsloth Unsloth is an open‑source framework for fine‑tuning and reinforcement learning that trains LLMs (like Llama, Mistral, Gemma, DeepSeek, and more) up to 2× faster with up to 70% less VRAM, while providing a streamlined, Hugging Face–compatible workflow for training, evaluation, and deployment. For more information, see [Unsloth Integration](unsloth_integration). ## Tool Calling with DPO The [`DPOTrainer`] fully supports fine-tuning models with _tool calling_ capabilities. In this case, each dataset example should include: * The conversation messages (prompt, chosen and rejected), including any tool calls (`tool_calls`) and tool responses (`tool` role messages) * The list of available tools in the `tools` column, typically provided as JSON schemas For details on the expected dataset structure, see the [Dataset Format — Tool Calling](dataset_formats#tool-calling) section. ## Training Vision Language Models [`DPOTrainer`] fully supports training Vision-Language Models (VLMs). To train a VLM, provide a dataset with either an `image` column (single image per sample) or an `images` column (list of images per sample). For more information on the expected dataset structure, see the [Dataset Format — Vision Dataset](dataset_formats#vision-dataset) section. An example of such a dataset is the [RLAIF-V Dataset](https://huggingface.co/datasets/HuggingFaceH4/rlaif-v_formatted) dataset. ```python from trl import DPOConfig, DPOTrainer from datasets import load_dataset trainer = DPOTrainer( model="Qwen/Qwen2.5-VL-3B-Instruct", args=DPOConfig(max_length=None), train_dataset=load_dataset("HuggingFaceH4/rlaif-v_formatted", split="train"), ) trainer.train() ``` > [!TIP] > For VLMs, truncating may remove image tokens, leading to errors during training. To avoid this, set `max_length=None` in the [`DPOConfig`]. This allows the model to process the full sequence length without truncating image tokens. > > ```python > DPOConfig(max_length=None, ...) > ``` > > Only use `max_length` when you've verified that truncation won't remove image tokens for the entire dataset. ## DPOTrainer [[autodoc]] DPOTrainer - train - save_model - push_to_hub ## DPOConfig [[autodoc]] DPOConfig ================================================ FILE: docs/source/example_overview.md ================================================ # Examples This directory contains a collection of examples that demonstrate how to use the TRL library for various applications. We provide both **scripts** for advanced use cases and **notebooks** for an easy start and interactive experimentation. The notebooks are self-contained and can run on **free Colab**, while the scripts can run on **single GPU, multi-GPU, or DeepSpeed** setups. **Getting Started** Install TRL and additional dependencies as follows: ```bash pip install --upgrade trl[quantization] ``` Check for additional optional dependencies [here](https://github.com/huggingface/trl/blob/main/pyproject.toml). For scripts, you will also need an 🤗 Accelerate config (recommended for multi-gpu settings): ```bash accelerate config # will prompt you to define the training configuration ``` This allows you to run scripts with `accelerate launch` in single or multi-GPU settings. ## Notebooks These notebooks are easier to run and are designed for quick experimentation with TRL. The list of notebooks can be found in the [`trl/examples/notebooks/`](https://github.com/huggingface/trl/tree/main/examples/notebooks/) directory. | Notebook | Description | Open in Colab | |----------|-------------|---------------| | [`grpo_trl_lora_qlora.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_trl_lora_qlora.ipynb) | GRPO using QLoRA on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_trl_lora_qlora.ipynb) | | [`grpo_functiongemma_browsergym_openenv.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb) | GRPO on FunctionGemma in the BrowserGym environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb) | | [`grpo_agent.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_agent.ipynb) | GRPO for agent training | Not available due to OOM with Colab GPUs | | [`grpo_rnj_1_instruct.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_rnj_1_instruct.ipynb) | GRPO rnj-1-instruct with QLoRA using TRL on Colab to add reasoning capabilities | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_rnj_1_instruct.ipynb) | | [`sft_ministral3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_ministral3_vl.ipynb) | Supervised Fine-Tuning (SFT) Ministral 3 with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_ministral3_vl.ipynb) | | [`grpo_ministral3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_ministral3_vl.ipynb) | GRPO Ministral 3 with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_ministral3_vl.ipynb) | | [`openenv_sudoku_grpo.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/openenv_sudoku_grpo.ipynb) | GRPO to play Sudoku on an OpenEnv environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb) | | [`openenv_wordle_grpo.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/openenv_wordle_grpo.ipynb) | GRPO to play Worldle on an OpenEnv environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/openenv_wordle_grpo.ipynb) | | [`sft_nemotron_3.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_nemotron_3.ipynb) | SFT with LoRA on NVIDIA Nemotron 3 models | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_nemotron_3.ipynb) | | [`sft_trl_lora_qlora.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_trl_lora_qlora.ipynb) | Supervised Fine-Tuning (SFT) using QLoRA on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_trl_lora_qlora.ipynb) | | [`sft_qwen_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_qwen_vl.ipynb) | Supervised Fine-Tuning (SFT) Qwen3-VL with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_qwen_vl.ipynb) | | [`sft_tool_calling.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_tool_calling.ipynb) | Teaching tool calling to a model without native tool-calling support using SFT with QLoRA | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_tool_calling.ipynb) | | [`grpo_qwen3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_qwen3_vl.ipynb) | GRPO Qwen3-VL with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_qwen3_vl.ipynb) | ## Scripts Scripts are maintained in the [`trl/scripts`](https://github.com/huggingface/trl/blob/main/trl/scripts) and [`examples/scripts`](https://github.com/huggingface/trl/blob/main/examples/scripts) directories. They show how to use different trainers such as [`SFTTrainer`], [`PPOTrainer`], [`DPOTrainer`], [`GRPOTrainer`], and more. | File | Description | | --- | --- | | [`examples/scripts/bco.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/bco.py) | This script shows how to use the [`experimental.kto.KTOTrainer`] with the BCO loss to fine-tune a model to increase instruction-following, truthfulness, honesty, and helpfulness using the [openbmb/UltraFeedback](https://huggingface.co/datasets/openbmb/UltraFeedback) dataset. | | [`examples/scripts/cpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/cpo.py) | This script shows how to use the [`experimental.cpo.CPOTrainer`] to fine-tune a model to increase helpfulness and harmlessness using the [Anthropic/hh-rlhf](https://huggingface.co/datasets/Anthropic/hh-rlhf) dataset. | | [`trl/scripts/dpo.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/dpo.py) | This script shows how to use the [`DPOTrainer`] to fine-tune a model. | | [`examples/scripts/dpo_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/dpo_vlm.py) | This script shows how to use the [`DPOTrainer`] to fine-tune a Vision Language Model to reduce hallucinations using the [openbmb/RLAIF-V-Dataset](https://huggingface.co/datasets/openbmb/RLAIF-V-Dataset) dataset. | | [`examples/scripts/evals/judge_tldr.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/evals/judge_tldr.py) | This script shows how to use [`experimental.judges.HfPairwiseJudge`] or [`experimental.judges.OpenAIPairwiseJudge`] to judge model generations. | | [`examples/scripts/gkd.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/gkd.py) | This script shows how to use the [`experimental.gkd.GKDTrainer`] to fine-tune a model. | | [`trl/scripts/grpo.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/grpo.py) | This script shows how to use the [`GRPOTrainer`] to fine-tune a model. | | [`trl/scripts/grpo_agent.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/grpo_agent.py) | This script shows how to use the [`GRPOTrainer`] to fine-tune a model to enable agentic usage. | | [`examples/scripts/grpo_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/grpo_vlm.py) | This script shows how to use the [`GRPOTrainer`] to fine-tune a multimodal model for reasoning using the [lmms-lab/multimodal-open-r1-8k-verified](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified) dataset. | | [`examples/scripts/gspo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/gspo.py) | This script shows how to use GSPO via the [`GRPOTrainer`] to fine-tune model for reasoning using the [AI-MO/NuminaMath-TIR](https://huggingface.co/datasets/AI-MO/NuminaMath-TIR) dataset. | | [`examples/scripts/gspo_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/gspo_vlm.py) | This script shows how to use GSPO via the [`GRPOTrainer`] to fine-tune a multimodal model for reasoning using the [lmms-lab/multimodal-open-r1-8k-verified](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified) dataset. | | [`examples/scripts/kto.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/kto.py) | This script shows how to use the [`experimental.kto.KTOTrainer`] to fine-tune a model. | | [`examples/scripts/mpo_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/mpo_vlm.py) | This script shows how to use MPO via the [`DPOTrainer`] to align a model based on preferences using the [HuggingFaceH4/rlaif-v_formatted](https://huggingface.co/datasets/HuggingFaceH4/rlaif-v_formatted) dataset and a set of loss weights with weights. | | [`examples/scripts/nash_md.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/nash_md.py) | This script shows how to use the [`experimental.nash_md.NashMDTrainer`] to fine-tune a model. | | [`examples/scripts/nemo_gym/train_multi_environment.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/nemo_gym/train_multi_environment.py) | This script shows how to use the [`GRPOTrainer`] to train language models in NVIDIA NeMo-Gym environments. Supports multi-turn and tool calling environments, and multi-environment training. See the [NeMo-Gym Integration](nemo_gym) guide for setup and usage. | | [`examples/scripts/online_dpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/online_dpo.py) | This script shows how to use the [`experimental.online_dpo.OnlineDPOTrainer`] to fine-tune a model. | | [`examples/scripts/online_dpo_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/online_dpo_vlm.py) | This script shows how to use the [`experimental.online_dpo.OnlineDPOTrainer`] to fine-tune a a Vision Language Model. | | [`examples/scripts/openenv/browsergym.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/browsergym.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's BrowserGym environment and vLLM for VLMs | | [`examples/scripts/openenv/browsergym_llm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/browsergym_llm.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's BrowserGym environment and vLLM for LLMs | | [`examples/scripts/openenv/carla.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/carla.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's CARLA environment for autonomous driving scenarios. | | [`examples/scripts/openenv/catch.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/catch.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's Catch environment (OpenSpiel) and vLLM | | [`examples/scripts/openenv/echo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/echo.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's Echo environment and vLLM. | | [`examples/scripts/openenv/sudoku.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/sudoku.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's Sudoku environment and vLLM. | | [`examples/scripts/openenv/wordle.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/wordle.py) | Simple script to run GRPO training via the [`GRPOTrainer`] with OpenEnv's Wordle environment and vLLM. | | [`examples/scripts/orpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/orpo.py) | This script shows how to use the [`experimental.orpo.ORPOTrainer`] to fine-tune a model to increase helpfulness and harmlessness using the [Anthropic/hh-rlhf](https://huggingface.co/datasets/Anthropic/hh-rlhf) dataset. | | [`examples/scripts/ppo/ppo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/ppo/ppo.py) | This script shows how to use the [`experimental.ppo.PPOTrainer`] to fine-tune a model to improve its ability to continue text with positive sentiment or physically descriptive language. | | [`examples/scripts/ppo/ppo_tldr.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/ppo/ppo_tldr.py) | This script shows how to use the [`experimental.ppo.PPOTrainer`] to fine-tune a model to improve its ability to generate TL;DR summaries. | | [`examples/scripts/prm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/prm.py) | This script shows how to use the [`experimental.prm.PRMTrainer`] to fine-tune a Process-supervised Reward Model (PRM). | | [`examples/scripts/reward_modeling.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/reward_modeling.py) | This script shows how to use the [`RewardTrainer`] to train an Outcome Reward Model (ORM) on your own dataset. | | [`examples/scripts/rloo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/rloo.py) | This script shows how to use the [`RLOOTrainer`] to fine-tune a model to improve its ability to solve math questions. | | [`examples/scripts/sft.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a model. | | [`examples/scripts/sft_gemma3.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_gemma3.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a Gemma 3 model. | | [`examples/scripts/sft_nemotron_3.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_nemotron_3.py) | This script shows how to use the [`SFTTrainer`] to fine-tune an NVIDIA Nemotron 3 model. | | [`examples/scripts/sft_tiny_aya_tool_calling.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_tiny_aya_tool_calling.py) | This script shows how to use the [`SFTTrainer`] to teach tool calling to a model without native tool-calling support using the [bebechien/SimpleToolCalling](https://huggingface.co/datasets/bebechien/SimpleToolCalling) dataset. | | [`examples/scripts/sft_video_llm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_video_llm.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a Video Language Model. | | [`examples/scripts/sft_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_vlm.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a Vision Language Model in a chat setting. The script has only been tested with [LLaVA 1.5](https://huggingface.co/llava-hf/llava-1.5-7b-hf), [LLaVA 1.6](https://huggingface.co/llava-hf/llava-v1.6-mistral-7b-hf), and [Llama-3.2-11B-Vision-Instruct](https://huggingface.co/meta-llama/Llama-3.2-11B-Vision-Instruct) models, so users may see unexpected behaviour in other model architectures. | | [`examples/scripts/sft_vlm_gemma3.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_vlm_gemma3.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a Gemma 3 model on vision to text tasks. | | [`examples/scripts/sft_vlm_smol_vlm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/sft_vlm_smol_vlm.py) | This script shows how to use the [`SFTTrainer`] to fine-tune a SmolVLM model. | | [`examples/scripts/xpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/xpo.py) | This script shows how to use the [`experimental.xpo.XPOTrainer`] to fine-tune a model. | ## Distributed Training (for scripts) You can run scripts on multiple GPUs with 🤗 Accelerate: ```shell accelerate launch --config_file=examples/accelerate_configs/multi_gpu.yaml --num_processes {NUM_GPUS} path_to_script.py --all_arguments_of_the_script ``` For DeepSpeed ZeRO-{1,2,3}: ```shell accelerate launch --config_file=examples/accelerate_configs/deepspeed_zero{1,2,3}.yaml --num_processes {NUM_GPUS} path_to_script.py --all_arguments_of_the_script ``` Adjust `NUM_GPUS` and `--all_arguments_of_the_script` as needed. ================================================ FILE: docs/source/experimental_overview.md ================================================ # Experimental This directory contains a minimal, clearly separated space for fast iteration on new ideas. > [!WARNING] > **Stability contract:** Anything under `trl.experimental` may change or be removed in *any* release (including patch versions) without prior deprecation. Do not rely on these APIs for production workloads. ## Promotion Path (Simple) 1. **Prototype outside the main repo:** Start development in your own fork or a separate repository to iterate quickly. 2. **Experimental inclusion:** Once it’s ready for early users, move the idea into `trl.experimental.`. 3. **Improve:** Add tests, a short doc/example, and demonstrate the usage. 4. **Promote:** Once the API proves stable and there is clear interest or adoption from the community, move it into `trl.` (stable module). ## FAQ **Why not just use branches?** Because branches are not shipped to users; experimental code inside the package lets early adopters try things and give feedback. **Can these APIs change or vanish without warning?** Yes. Anything inside `trl.experimental` can change or disappear in *any* release. **Should I use this in production?** Only if you are fine with updating your code quickly when things change. **Will maintainers promptly fix issues in `trl.experimental`?** Not necessarily. The experimental module is a playground for new ideas, and maintainers may not prioritize bug fixes or feature requests there. Issues may remain unresolved until (or unless) the feature graduates to the stable API. **How to silence the runtime notice?** Use: `export TRL_EXPERIMENTAL_SILENCE=1`. ================================================ FILE: docs/source/gfpo.md ================================================ # GFPO This feature implements the GFPO algorithm to enforce concise reasoning in the model's output generation, as proposed in the paper [Sample More to Think Less: Group Filtered Policy Optimization for Concise Reasoning](https://huggingface.co/papers/2508.09726). ## Usage To activate GFPO in [`GFPOTrainer`]: - set `num_remains_in_group` in [`GFPOConfig`] - define a group filter function and set it to `group_filter_func` in [`GFPOTrainer`]. `group_filter_func` will score the `num_generations` completions and The GFPOTrainer filters groups according to their scores to get top `num_remains_in_group` completions as a new group. Model will be trained on the filtered group. ```python # train_gfpo.py from trl.experimental.gfpo import GFPOConfig, GFPOTrainer # dummy group filter to scores the completions based on its indice in group class GroupFilter: def __call__(self, group_completions, group_rewards, **kwargs): group_scores = [] for completions, rewards in zip(group_completions, group_rewards): scores = [float(i) for i in range(len(completions))] group_scores.append(scores) return group_scores training_args = GFPOConfig( output_dir="Qwen3-0.6B-GFPO", per_device_train_batch_size=4, num_remains_in_group=2, bf16=True, ) trainer = GFPOTrainer( model="Qwen/Qwen3-0.6B", reward_funcs=..., train_dataset=..., args=training_args, group_filter_func=GroupFilter(), ) trainer.train() ``` ## GFPOTrainer [[autodoc]] experimental.gfpo.GFPOTrainer - train - save_model - push_to_hub ## GFPOConfig [[autodoc]] experimental.gfpo.GFPOConfig ================================================ FILE: docs/source/gkd_trainer.md ================================================ # Generalized Knowledge Distillation Trainer [![model badge](https://img.shields.io/badge/All_models-GKD-blue)](https://huggingface.co/models?other=gkd,trl) ## Overview Generalized Knowledge Distillation (GKD) was proposed in [On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes](https://huggingface.co/papers/2306.13649) by Rishabh Agarwal, Nino Vieillard, Yongchao Zhou, Piotr Stanczyk, Sabela Ramos, Matthieu Geist, and Olivier Bachem. The abstract from the paper is the following: > Knowledge distillation (KD) is widely used for compressing a teacher model to reduce its inference cost and memory footprint, by training a smaller student model. However, current KD methods for auto-regressive sequence models suffer from distribution mismatch between output sequences seen during training and those generated by the student during inference. To address this issue, we introduce Generalized Knowledge Distillation (GKD). Instead of solely relying on a fixed set of output sequences, GKD trains the student on its self-generated output sequences by leveraging feedback from the teacher on such sequences. Unlike supervised KD approaches, GKD also offers the flexibility to employ alternative loss functions between the student and teacher, which can be useful when the student lacks the expressivity to mimic the teacher's distribution. Furthermore, GKD facilitates the seamless integration of distillation with RL fine-tuning (RLHF). We demonstrate the efficacy of GKD for distilling auto-regressive language models on summarization, translation, and arithmetic reasoning tasks, and task-agnostic distillation for instruction-tuning. The key aspects of GKD are: 1. It addresses the train-inference distribution mismatch in auto-regressive sequence models by training the student model on its self-generated output sequences. 2. GKD allows flexibility in choosing different divergence measures between student and teacher models via the generalized Jensen-Shannon Divergence (JSD), which can be useful when the student lacks the capacity to fully mimic the teacher. This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif) and [Lewis Tunstall](https://huggingface.co/lewtun). ## Usage tips The [`experimental.gkd.GKDTrainer`] is a wrapper around the [`SFTTrainer`] class that takes in a teacher model argument. It needs three parameters to be set via the [`experimental.gkd.GKDConfig`] namely: * `lmbda`: controls the student data fraction, i.e., the proportion of on-policy student-generated outputs. When `lmbda=0.0`, the loss reduces to supervised JSD where the student is trained with the token-level probabilities of the teacher. When `lmbda=1.0`, the loss reduces to on-policy JSD, where the student generates output sequences and token-specific feedback on these sequences from the teacher. For values in between [0, 1] it is random between the two based on the `lmbda` value for each batch. * `seq_kd`: controls whether to perform Sequence-Level KD (can be viewed as supervised FT on teacher-generated out). When `seq_kd=True` and `lmbda=0.0`, the loss reduces to supervised JSD, where the teacher generates output sequences and the student receives token-specific feedback on these sequences from the teacher. * `beta`: controls the interpolation in the generalized Jensen-Shannon Divergence. When `beta=0.0` the loss approximates forward KL divergence, while for `beta=1.0` the loss approximates reverse KL divergence. For values in between [0, 1] it interpolates between the two. The authors find that on-policy data (high `lmbda`) performs better and the optimal `beta` varied depending on the task and evaluation method. > [!WARNING] > Make sure that `attn_implementation="kernels-community/flash-attn2"` when training [Gemma models](https://huggingface.co/models?other=gemma2). Otherwise you will encounter NaNs in the logits due to the [soft capping technique](https://huggingface.co/blog/gemma2#soft-capping-and-attention-implementations) adopted by this architecture. The basic API is as follows: ```python from datasets import Dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl.experimental.gkd import GKDConfig, GKDTrainer NUM_DUMMY_SAMPLES = 100 tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") # The model to optimise model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") # The teacher model to calculate the KL divergence against teacher_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-1.5B-Instruct") train_dataset = Dataset.from_dict( { "messages": [ [ {"role": "user", "content": "Hi, how are you?"}, {"role": "assistant", "content": "I'm great thanks"}, ] ] * NUM_DUMMY_SAMPLES } ) eval_dataset = Dataset.from_dict( { "messages": [ [ {"role": "user", "content": "What colour is the sky?"}, {"role": "assistant", "content": "The sky is blue"}, ] ] * NUM_DUMMY_SAMPLES } ) training_args = GKDConfig(output_dir="gkd-model", per_device_train_batch_size=1) trainer = GKDTrainer( model=model, teacher_model=teacher_model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset, eval_dataset=eval_dataset, ) trainer.train() ``` ### Expected dataset type The dataset should be formatted as a list of "messages" where each message is a list of dictionaries with the following keys: * `role`: either `system`, `assistant` or `user` * `content`: the message content ## GKDTrainer [[autodoc]] experimental.gkd.GKDTrainer - train - save_model - push_to_hub ## GKDConfig [[autodoc]] experimental.gkd.GKDConfig ================================================ FILE: docs/source/gold_trainer.md ================================================ # General Online Logit Distillation (GOLD) Trainer [![All_models-GOLD-blue](https://img.shields.io/badge/All_models-GOLD-blue)](https://huggingface.co/models?other=sft,gold) ## Overview General Online Logit Distillation (GOLD) is an extension of Universal Logit Distillation (ULD) that supports student/teacher pairs with different tokenizers. It aligns the textual spans produced by both tokenizers and merges the associated logits so no completion tokens are dropped. This enables cross-tokenizer knowledge distillation, including mixed model families (for example, LLaMA students with Qwen teachers). Key capabilities: 1. **Cross-tokenizer alignment** – GOLD incrementally decodes the student and teacher tokens, groups passages with the same visible text, and merges probabilities inside each group. This guarantees loss terms are computed over the full completion even when token boundaries differ. 2. **Hybrid ULD loss** – when `uld_use_hybrid_loss` is enabled, GOLD compares exact vocabulary matches directly and falls back to the original sorted-probability ULD loss for unmatched tokens. This improves stability for students whose vocabularies only partially overlap with the teacher. 3. **Seamless integration with GKD** – GOLD inherits the on-policy vs. off-policy scheduling from the [`experimental.gkd.GKDTrainer`], so you can combine sequence-level KD, generalized JSD, and cross-tokenizer distillation in a single training run. > [!NOTE] > GOLD is currently part of the `trl.experimental` namespace. APIs may change without notice while the feature is iterated on. ## Usage tips The [`GOLDTrainer`] subclasses [`SFTTrainer`] and accepts the same datasets as other TRL trainers (lists of ChatML style messages). Important configuration flags on [`GOLDConfig`] include: * `use_uld_loss` – toggles Universal Logit Distillation. Set this to `True` for cross-tokenizer setups. * `teacher_tokenizer_name_or_path` – required when `use_uld_loss=True`; GOLD uses the teacher tokenizer to align tokens. * `uld_use_hybrid_loss`, `uld_hybrid_matched_weight`, `uld_hybrid_unmatched_weight` – enables and weights the hybrid matched/unmatched loss. * `beta`, `lmbda`, `seq_kd` – inherited from [`experimental.gkd.GKDConfig`], controlling the generalized JSD interpolation and on-policy sampling ratio. * `num_generations`, `generation_batch_size` – control buffered rollout generation across gradient accumulation windows. `generation_batch_size` is the number of unique prompts per worker per optimizer step. * `model_revision` – controls which student model revision GOLD loads for training and generation. A minimal end-to-end example: ```python from datasets import load_dataset from trl.experimental.gold import GOLDConfig, GOLDTrainer train_dataset = load_dataset( "HuggingFaceTB/OpenR1-Math-220k-default-verified", "all", split="train[:1024]", ) trainer = GOLDTrainer( model="meta-llama/Llama-3.2-1B-Instruct", teacher_model="Qwen/Qwen2.5-0.5B-Instruct", args=GOLDConfig(output_dir="gold-model", use_uld_loss=True, teacher_tokenizer_name_or_path="Qwen/Qwen2.5-0.5B-Instruct"), train_dataset=train_dataset, ) trainer.train() ``` For quick-start workflows you can rely on string identifiers as shown above—the trainer will load the model and tokenizer for you. Explicitly instantiating `AutoModelForCausalLM`, `AutoTokenizer`, or populating `GOLDConfig` is recommended only for advanced use cases where you need fine-grained control over initialization. A more explicit setup might look like this when you need to customise model loading, tokenizer settings, or training arguments: ```python from datasets import load_dataset from trl import GOLDConfig, GOLDTrainer from transformers import AutoModelForCausalLM, AutoTokenizer student_name = "meta-llama/Llama-3.2-1B-Instruct" teacher_name = "Qwen/Qwen2.5-0.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(student_name) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained(student_name) teacher_model = AutoModelForCausalLM.from_pretrained(teacher_name) train_dataset = load_dataset( "HuggingFaceTB/Countdown-Task-GOLD", "verified_Qwen2.5-0.5B-Instruct", split="train", ) training_args = GOLDConfig( output_dir="gold-model", per_device_train_batch_size=1, teacher_model_name_or_path=teacher_name, teacher_tokenizer_name_or_path=teacher_name, use_uld_loss=True, uld_use_hybrid_loss=True, ) trainer = GOLDTrainer( model=model, teacher_model=teacher_model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset, ) trainer.train() ``` > [!NOTE] > GOLD buffers one full optimizer-window generation batch (`per_device_train_batch_size * gradient_accumulation_steps`) > and reuses it across accumulation steps. If the final batch is undersized, GOLD warns and drops that last batch > (`Dropping last batch due to unexpected batch size`). Set `dataloader_drop_last=True` to avoid this warning. ### Expected dataset type GOLD requires a [conversational](dataset_formats#conversational) [language modeling](dataset_formats#language_modeling) dataset, e.g.: ```python {"messages": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}]} ``` `GOLDTrainer` keeps the raw messages so the ChatML collator can construct prompts and completions with the correct boundaries. ## How Token Merging Works When student and teacher use different tokenizers, the same text may be split differently: - **Student**: `"Hugging Face"` → 1 token - **Teacher**: `"Hugging"`, `" Face"` → 2 tokens GOLD aligns these sequences and merges the teacher's multi-token probabilities into a single distribution that can be compared with the student's single-token distribution. ### Probability Merging For a teacher sequence of tokens `[token₀, token₁, ..., tokenₖ]` that maps to a single student token, GOLD computes: ``` P_merged(y) = P(y | context) × P(token₁ | token₀, context) × ... × P(tokenₖ | ..., context) ``` where: - `P(y | context)` is the marginal probability distribution over all vocabulary tokens at the first position - `P(tokenᵢ | ..., context)` are **scalar** conditional probabilities of the actual tokens that were generated **Key insight**: Only the conditional probabilities of the **actual continuation tokens** are extracted as scalars. The full marginal distribution at the first position is then scaled by multiplying these scalar probabilities. This ensures: 1. **Correct joint probability** for the actual generated sequence (by the chain rule) 2. **Reasonable approximation** for counterfactual tokens (scaled by the same continuation likelihood) 3. **Unnormalized distributions** that preserve the correct relative probabilities for ULD loss computation ### Example Given: ``` P(x₀): ["HF": 0.6, "is": 0.3, "cool": 0.1] P(x₁ | "HF"): ["HF": 0.05, "is": 0.9, "cool": 0.05] ``` If tokens 0 and 1 are merged, and the actual sequence was `["HF", "is"]`: ``` P_merged("HF") = 0.6 × 0.9 = 0.54 ✓ (correct joint probability) P_merged("is") = 0.3 × 0.9 = 0.27 P_merged("cool") = 0.1 × 0.9 = 0.09 ``` The merged distribution is unnormalized (sums to 0.81), but this is intentional and correct for ULD loss computation, which uses sorting and L1 distance. ## GOLDTrainer [[autodoc]] experimental.gold.GOLDTrainer - train - generate_on_policy_outputs - save_model - push_to_hub ## GOLDConfig [[autodoc]] experimental.gold.GOLDConfig ================================================ FILE: docs/source/grpo_trainer.md ================================================ # GRPO Trainer [![model badge](https://img.shields.io/badge/All_models-GRPO-blue)](https://huggingface.co/models?other=grpo,trl) ## Overview TRL supports the GRPO Trainer for training language models, as described in the paper [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300) by [Zhihong Shao](https://huggingface.co/syhia), [Peiyi Wang](https://huggingface.co/peiyiwang89), [Qihao Zhu](https://huggingface.co/zqh11), Runxin Xu, [Junxiao Song](https://huggingface.co/haha-point), Mingchuan Zhang, Y. K. Li, Y. Wu, [Daya Guo](https://huggingface.co/guoday). The abstract from the paper is the following: > Mathematical reasoning poses a significant challenge for language models due to its complex and structured nature. In this paper, we introduce DeepSeekMath 7B, which continues pre-training DeepSeek-Coder-Base-v1.5 7B with 120B math-related tokens sourced from Common Crawl, together with natural language and code data. DeepSeekMath 7B has achieved an impressive score of 51.7% on the competition-level MATH benchmark without relying on external toolkits and voting techniques, approaching the performance level of Gemini-Ultra and GPT-4. Self-consistency over 64 samples from DeepSeekMath 7B achieves 60.9% on MATH. The mathematical reasoning capability of DeepSeekMath is attributed to two key factors: First, we harness the significant potential of publicly available web data through a meticulously engineered data selection pipeline. Second, we introduce Group Relative Policy Optimization (GRPO), a variant of Proximal Policy Optimization (PPO), that enhances mathematical reasoning abilities while concurrently optimizing the memory usage of PPO. This post-training method was contributed by [Quentin Gallouédec](https://huggingface.co/qgallouedec). ## Quick start This example demonstrates how to train a model using the GRPO method. We train a [Qwen 0.5B Instruct model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) with the prompts from the [DeepMath-103K dataset](https://huggingface.co/datasets/trl-lib/DeepMath-103K). You can view the data in the dataset here: Below is the script to train the model. ```python # train_grpo.py from datasets import load_dataset from trl import GRPOTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = GRPOTrainer( model="Qwen/Qwen2-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_grpo.py ``` Distributed across 8 GPUs, the training takes approximately 1 day. ![GRPO curves](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/grpo_curves.png) ## Looking deeper into the GRPO method GRPO is an online learning algorithm, meaning it improves iteratively by using the data generated by the trained model itself during training. The intuition behind GRPO objective is to maximize the advantage of the generated completions, while ensuring that the model remains close to the reference policy. To understand how GRPO works, it can be broken down into four main steps: **Generating completions**, **computing the advantage**, **estimating the KL divergence**, and **computing the loss**. ![GRPO visual](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/grpo_visual.png) ### Generating completions At each training step, we sample a batch of prompts and generate a set of \\( G \\) completions for each prompt (denoted as \\( o_i \\)). ### Computing the advantage For each of the \\( G \\) sequences, we compute the reward using a reward model or reward function. To align with the comparative nature of reward models—typically trained on datasets of comparisons between outputs for the same question—the advantage is calculated to reflect these relative comparisons. It is normalized as follows: $$\hat{A}_{i,t} = \frac{r_i - \text{mean}(\mathbf{r})}{\text{std}(\mathbf{r})}$$ This approach gives the method its name: **Group Relative Policy Optimization (GRPO)**. > [!TIP] > It was shown in the paper [Understanding R1-Zero-Like Training: A Critical Perspective](https://huggingface.co/papers/2503.20783) that scaling by \\( \text{std}(\mathbf{r}) \\) may cause a question-level difficulty bias. You can disable this scaling by setting `scale_rewards=False` in [`GRPOConfig`]. > Note that turning off std-based scaling also removes variance normalization, so update magnitudes depend directly on the raw reward scale and batch composition. > [!TIP] > As shown in [Part I: Tricks or Traps? A Deep Dive into RL for LLM Reasoning (Lite PPO)](https://huggingface.co/papers/2508.08221), calculating the mean at the local (group) level and the standard deviation at the global (batch) level enables more robust reward shaping. You can use this scaling strategy by setting `scale_rewards="batch"` in [`GRPOConfig`]. ### Estimating the KL divergence KL divergence is estimated using the approximator introduced by [Schulman et al. (2020)](http://joschu.net/blog/kl-approx.html). The approximator is defined as follows: $$\mathbb{D}_{\text{KL}}\left[\pi_\theta \|\pi_{\text{ref}}\right] = \frac{\pi_{\text{ref}}(o_{i,t} \mid q, o_{i, [!TIP] > Note that compared to the original formulation in [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300), we don't scale by \\( \frac{1}{|o_i|} \\) because it was shown in the paper [Understanding R1-Zero-Like Training: A Critical Perspective](https://huggingface.co/papers/2503.20783) that this introduces a response-level length bias. More details in [loss types](#loss-types). > [!TIP] > Note that compared to the original formulation in [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300), we use \\( \beta = 0.0 \\) by default, meaning that the KL divergence term is not used. This choice is motivated by several recent studies (e.g., [Open-Reasoner-Zero: An Open Source Approach to Scaling Up Reinforcement Learning on the Base Model](https://huggingface.co/papers/2503.24290)) which have shown that the KL divergence term is not essential for training with GRPO. As a result, it has become common practice to exclude it (e.g. [Understanding R1-Zero-Like Training: A Critical Perspective](https://huggingface.co/papers/2503.20783), [DAPO: An Open-Source LLM Reinforcement Learning System at Scale](https://huggingface.co/papers/2503.14476)). If you wish to include the KL divergence term, you can set `beta` in [`GRPOConfig`] to a non-zero value. In the original paper, this formulation is generalized to account for multiple updates after each generation (denoted \\( \mu \\), can be set with `num_iterations` in [`GRPOConfig`]) by leveraging the **clipped surrogate objective**: $$ \mathcal{L}_{\text{GRPO}}(\theta) = - \frac{1}{\sum_{i=1}^G |o_i|} \sum_{i=1}^G \sum_{t=1}^{|o_i|} \left[ \min \left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \hat{A}_{i,t}, \, \text{clip}\left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})}, 1 - \epsilon, 1 + \epsilon \right) \hat{A}_{i,t} \right) - \beta \mathbb{D}_{\text{KL}}\left[\pi_\theta \| \pi_{\text{ref}}\right] \right], $$ where \\(\text{clip}(\cdot, 1 - \epsilon, 1 + \epsilon) \\) ensures that updates do not deviate excessively from the reference policy by bounding the policy ratio between \\( 1 - \epsilon \\) and \\( 1 + \epsilon \\). When \\( \mu = 1 \\) (default in TRL), the clipped surrogate objective simplifies to the original objective. #### Loss Types Several formulations of the objective have been proposed in the literature. Initially, the objective of GRPO was defined as follows: $$ \mathcal{L}_{\text{GRPO}}(\theta) = - \frac{1}{G} \sum_{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} l_{i,t}, $$ where $$ l_{i,t} = \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\left[\pi_\theta(o_{i,t} \mid q, o_{i,< t})\right]_{\text{no grad}}} \hat{A}_{i,t} - \beta \mathbb{D}_{\text{KL}}\left[\pi_\theta \| \pi_{\text{ref}}\right]. $$ The [DAPO paper](https://huggingface.co/papers/2503.14476) highlights the limitations of the GRPO algorithm’s sample-level loss in long-CoT scenarios, where longer responses are under-penalized, leading to poorer quality outputs. The proposed solution is a token-level normalization, which better handles longer sequences by assigning more balanced rewards to individual tokens, regardless of response length: $$ \mathcal{L}_{\text{DAPO}}(\theta) = - \frac{1}{\sum_{i=1}^G |o_i|} \sum_{i=1}^G \sum_{t=1}^{|o_i|} l_{i,t}, $$ To use this formulation, set `loss_type="dapo"` in [`GRPOConfig`]. Furthermore, it was demonstrated in the paper [Understanding R1-Zero-Like Training: A Critical Perspective](https://huggingface.co/papers/2503.20783) that the initial GRPO formulation introduces a response length bias. They show that while the DAPO formulation reduces this bias, it does not eliminate it completely. To fully remove this bias, they propose dividing by a constant instead of the sequence length, resulting in the following formulation: $$ \mathcal{L}_{\text{Dr. GRPO}}(\theta) = - \frac{1}{LG} \sum_{i=1}^G \sum_{t=1}^{|o_i|} l_{i,t}, $$ This constant is recommended to be the maximum completion length. To use this formulation, set `loss_type="dr_grpo"` in the [`GRPOConfig`]. Alternatively, in the [SAPO paper](https://huggingface.co/papers/2511.20347), the Qwen team proposes replacing the "hard" clipping mechanism of GRPO with a smooth, temperature-controlled soft gating mechanism. While GRPO zeroes out gradients when the policy deviates too far from the reference, SAPO uses a soft trust region that smoothly decays the gradient weight. This allows the model to retain useful learning signals from "near-on-policy" tokens while suppressing noise from extreme deviations. The loss function is defined as: $$ \mathcal{L}_{\text{SAPO}}(\theta) = - \frac{1}{G} \sum_{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} f_{i,t} \left( \frac{\pi_\theta(o_{i,t} | q, o_{i, 0 \\ \tau_{\text{neg}}, & \text{otherwise} \end{cases} $$ They recommend using asymmetric temperatures, \\( \tau_{\text{neg}} > \tau_{\text{pos}} \\) (defaults are \\( \tau_{\text{pos}}=1.0, \tau_{\text{neg}}=1.05 \\) ). This ensures that the model is penalized more strictly for "bad" actions to prevent instability, while being more permissive with "good" actions. To use this formulation, set `loss_type="sapo"` in the [`GRPOConfig`]. ## Logged metrics While training and evaluating, we record the following reward metrics: - `num_tokens`: The total number of tokens processed so far, including both prompts and completions. When using tools, only non-tool tokens are counted. - `step_time`: The average time (in seconds) taken per training step (including generation). - `completions/mean_length`: The average length of generated completions. When using tools, only non-tool tokens are counted. - `completions/min_length`: The minimum length of generated completions. When using tools, only non-tool tokens are counted. - `completions/max_length`: The maximum length of generated completions. When using tools, only non-tool tokens are counted. - `completions/mean_terminated_length`: The average length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted. - `completions/min_terminated_length`: The minimum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted. - `completions/max_terminated_length`: The maximum length of generated completions that terminate with EOS. When using tools, only non-tool tokens are counted. - `completions/clipped_ratio`: The ratio of truncated (clipped) completions. - `reward/{reward_func_name}/mean`: The average reward from a specific reward function. - `reward/{reward_func_name}/std`: The standard deviation of the reward from a specific reward function. - `reward`: The overall average reward after summing rewards across functions (unweighted). - `reward_std`: The standard deviation of summed rewards across functions (unweighted), computed over the full batch. - `frac_reward_zero_std`: The fraction of samples in the generation batch with a reward std of zero, implying there is little diversity for that prompt (all answers are correct or incorrect). - `entropy`: Average entropy of token predictions across generated completions. (If `mask_truncated_completions=True`, masked sequences tokens are excluded.) - `kl`: The average KL divergence between the model and the reference model, calculated over generated completions. Logged only if `beta` is nonzero. - `clip_ratio/region_mean`: The ratio of token (or sequence, if `importance_sampling_level="sequence"`) probabilities where the GRPO objective is clipped to stay within the trust region: \\( \text{clip}\left( r_{i,t}(\theta), 1 - \epsilon_\mathrm{low}, 1 + \epsilon_\mathrm{high} \right)\,, \quad r_{i,t}(\theta) = \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \\). A higher value means more tokens are clipped, which constrains how much the policy $\pi_\theta$ can change. - `clip_ratio/low_mean`: The average ratio of token (or sequence, if `importance_sampling_level="sequence"`) probabilities that were clipped on the lower bound of the trust region: \\(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\\). - `clip_ratio/low_min`: The minimum ratio of token (or sequence, if `importance_sampling_level="sequence"`) probabilities that were clipped on the lower bound of the trust region: \\(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\\). - `clip_ratio/high_mean`: The average ratio of token (or sequence, if `importance_sampling_level="sequence"`) probabilities that were clipped on the upper bound of the trust region: \\(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\\). - `clip_ratio/high_max`: The maximum ratio of token (or sequence, if `importance_sampling_level="sequence"`) probabilities that were clipped on the upper bound of the trust region: \\(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\\). ## Customization ### Speed up training with vLLM-powered generation Generation is often the main bottleneck when training with online methods. To accelerate generation, you can use [vLLM](https://github.com/vllm-project/vllm), a high-throughput, low-latency inference engine for LLMs. To enable it, first install the package with ```shell pip install trl[vllm] ``` We support two ways of using vLLM during training: **server mode** and **colocate mode**. > [!TIP] > By default, Truncated Importance Sampling is activated for vLLM generation to address the generation-training mismatch that occurs when using different frameworks. This can be turned off by setting `vllm_importance_sampling_correction=False`. For more information, see [Truncated Importance Sampling](paper_index#truncated-importance-sampling) #### Option 1: Colocate mode In this mode, vLLM runs inside the trainer process and shares GPU memory with the training model. This avoids launching a separate server and can improve GPU utilization, but may lead to memory contention on the training GPUs. This is the default mode. ```python from trl import GRPOConfig training_args = GRPOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` #### Option 2: Server mode In this mode, vLLM runs in a separate process (and using separate GPUs) and communicates with the trainer via HTTP. This is ideal if you have dedicated GPUs for inference. 1. **Start the vLLM server**: ```bash trl vllm-serve --model ``` 2. **Enable server mode in your training script**: ```python from trl import GRPOConfig training_args = GRPOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` > [!WARNING] > Make sure that the server is using different GPUs than the trainer, otherwise you may run into NCCL errors. You can specify the GPUs to use with the `CUDA_VISIBLE_DEVICES` environment variable. > [!TIP] > Depending on the model size and the overall GPU memory requirements for training, you may need to adjust the `vllm_gpu_memory_utilization` parameter in [`GRPOConfig`] to avoid underutilization or out-of-memory errors. > > We provide a [HF Space](https://huggingface.co/spaces/trl-lib/recommend-vllm-memory) to help estimate the recommended GPU memory utilization based on your model configuration and experiment settings. Simply use it as follows to get `vllm_gpu_memory_utilization` recommendation: > > > > If the recommended value does not work in your environment, we suggest adding a small buffer (e.g., +0.05 or +0.1) to the recommended value to ensure stability. > > If you still find you are getting out-of-memory errors set `vllm_enable_sleep_mode` to True and the vllm parameters and cache will be offloaded during the optimization step. For more information, see [Reducing Memory Usage with vLLM Sleep Mode](reducing_memory_usage#vllm-sleep-mode). > [!TIP] > By default, GRPO uses `MASTER_ADDR=localhost` and `MASTER_PORT=12345` for vLLM, but you can override these values by setting the environment variables accordingly. For more information, see [Speeding up training with vLLM](speeding_up_training#vllm-for-fast-generation-in-online-methods). #### Dealing with the Training-Inference Mismatch While vLLM greatly accelerates inference, it also decouples the inference engine from the training engine. In theory these engines are mathematically identical, in practice however they can produce different outputs due to precision effects and hardware specific optimizations. This divergence reflects the different optimization objectives of the two systems. This divergence reflects the distinct optimization goals of the two systems. Inference engines aim to maximize sampling throughput, typically measured in tokens per second, while maintaining acceptable sampling fidelity. Training frameworks instead focus on numerical stability and precision for gradient computation, often using higher precision formats like FP32 for master weights and optimizer states. These differing priorities and constraints introduce an inevitable, albeit subtle, mismatch between training and inference. This mismatch leads to a biased gradient update which has been observed to destabilize training ([[1]](https://fengyao.notion.site/off-policy-rl)[[2]](https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda)[[3]](https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/#true-on-policy-rl)[[4]](https://huggingface.co/papers/2510.26788)[[5]](https://huggingface.co/papers/2510.18855)). For simplicity, consider the REINFORCE policy gradient: $$ \nabla_\theta \mathcal{J}(x,\theta) = \mathbb{E}_{y \sim \pi^\text{train}(\cdot \mid x,\theta)} \left[ \nabla_\theta \log \pi^\text{train}(y \mid x,\theta) \cdot R(x,y) \right] $$ Here \\( x \\) denotes prompts sampled from some data distribution, and \\( \pi^\text{train} \\) is the policy implemented by the training engine. With vLLM in the loop we obtain a separate inference policy \\( \pi^\text{inference} \\), so the effective policy gradient becomes $$ \nabla_\theta \mathcal{J}_{\text{biased}}(x,\theta) = \mathbb{E}_{y \sim \pi^\text{inference}(\cdot \mid x,\theta)} \left[ \nabla_\theta \log \pi^\text{train}(y \mid x,\theta) \cdot R(x,y) \right]. $$ This turns an otherwise on policy RL problem into an off policy one. The standard way to correct for this distribution shift is **importance sampling (IS)**. We provide two IS variants: [Truncated Importance Sampling (TIS)](paper_index#truncated-importance-sampling) and [Masked Importance Sampling (MIS)](paper_index#masked-importance-sampling). Both variants can be applied either at the token level or at the sequence level.Let \\( \rho \\) denote the importance weight, for example \\( \rho_t \\) per token or \\( \rho_{\text{seq}} \\) per sequence. Under TIS, ratios larger than `vllm_importance_sampling_cap` are clipped, $$ \rho \leftarrow \min(\rho, C). $$ Under MIS, ratios larger than `vllm_importance_sampling_cap` are set to zero, so those samples do not contribute to the gradient. In other words, large ratio samples are downweighted under TIS and discarded under MIS. The configuration flag `vllm_importance_sampling_mode` chooses both the IS variant (masking or truncation) and the granularity (token level or sequence level). Importance sampling is the principled algorithmic response to the training–inference mismatch. However, there are also more direct approaches that attempt to reduce the mismatch between the two engines themselves. Most of these are engineering solutions. For example, [MiniMax M1 uses an FP32 language model head](https://huggingface.co/papers/2506.13585) in the inference engine. Thinking Machines has explored [deterministic inference kernels](https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/), although this comes with a significant efficiency cost. vLLM has shown [bitwise consistent policies](https://blog.vllm.ai/2025/11/10/bitwise-consistent-train-inference.html) by building on the batch invariant deterministic kernels from Thinking Machines, but as of November 2025 there remains a substantial throughput penalty relative to standard vLLM inference. ### GRPO at scale: train a 70B+ Model on multiple nodes When training large models like **Qwen2.5-72B**, you need several key optimizations to make the training efficient and scalable across multiple GPUs and nodes. These include: - **DeepSpeed ZeRO Stage 3**: ZeRO leverages data parallelism to distribute model states (weights, gradients, optimizer states) across multiple GPUs and CPUs, reducing memory and compute requirements on each device. Since large models cannot fit on a single GPU, using ZeRO Stage 3 is required for training such models. For more details, see [DeepSpeed Integration](deepspeed_integration). - **Accelerate**: Accelerate is a library that simplifies distributed training across multiple GPUs and nodes. It provides a simple API to launch distributed training and handles the complexities of distributed training, such as data parallelism, gradient accumulation, and distributed data loading. For more details, see [Distributing Training](distributing_training). - **vLLM**: See the previous section on how to use vLLM to speed up generation. Below is an example SLURM script to train a 70B model with GRPO on multiple nodes. This script trains a model on 4 nodes and uses the 5th node for vLLM-powered generation. ```sh #!/bin/bash #SBATCH --nodes=5 #SBATCH --gres=gpu:8 # Get the list of allocated nodes NODELIST=($(scontrol show hostnames $SLURM_JOB_NODELIST)) # Assign the first 4 nodes for training and the 5th node for vLLM TRAIN_NODES="${NODELIST[@]:0:4}" # Nodes 0, 1, 2, 3 for training VLLM_NODE="${NODELIST[4]}" # Node 4 for vLLM # Run training on the first 4 nodes (Group 1) srun --nodes=4 --ntasks=4 --nodelist="${NODELIST[@]:0:4}" accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ --num_processes 32 \ --num_machines 4 \ --main_process_ip ${NODELIST[0]} \ --machine_rank $SLURM_PROCID \ --rdzv_backend c10d \ train_grpo.py \ --server_ip $VLLM_NODE & # Run vLLM server on the 5th node (Group 2) srun --nodes=1 --ntasks=1 --nodelist="${NODELIST[4]}" trl vllm-serve --model Qwen/Qwen2.5-72B --tensor_parallel_size 8 & wait ``` ```python import argparse from datasets import load_dataset from trl import GRPOTrainer, GRPOConfig from trl.rewards import accuracy_reward def main(): parser = argparse.ArgumentParser() parser.add_argument("--vllm_server_host", type=str, default="", help="The server IP") args = parser.parse_args() dataset = load_dataset("trl-lib/DeepMath-103K", split="train") training_args = GRPOConfig( per_device_train_batch_size=4, use_vllm=True, vllm_mode="server", vllm_server_host=args.vllm_server_host.replace("ip-", "").replace("-", "."), # from ip-X-X-X-X to X.X.X.X ) trainer = GRPOTrainer( model="Qwen/Qwen2.5-72B", args=training_args, reward_funcs=accuracy_reward, train_dataset=dataset ) trainer.train() if __name__=="__main__": main() ``` ### Using a custom reward function The [`GRPOTrainer`] supports using custom reward functions instead of dense reward models. To ensure compatibility, your reward function must satisfy the following requirements: Reward functions can be either synchronous Python callables or asynchronous `async def` coroutines. When you provide multiple asynchronous reward functions, they are awaited concurrently (run in parallel via `asyncio.gather`) so their latency overlaps. 1. **Input arguments**: - The function must accept the following as keyword arguments: - `prompts` (contains the prompts), - `completions` (contains the generated completions), - `completion_ids` (contains the tokenized completions), - `trainer_state` ([`~transformers.TrainerState`]): The current state of the trainer. This can be used to implement dynamic reward functions, such as curriculum learning, where the reward is adjusted based on the training progress. - `log_extra`: a callable `log_extra(column: str, values: list)` to add extra columns to the completions table. See Example 6. In distributed training, it's important that all processes log the same set of keys. - `log_metric`: a callable `log_metric(name: str, value: float)` to log scalar metrics as plots alongside `kl`, `entropy`, etc. See Example 6. In distributed training, it's important that all processes log the same set of keys. - All column names (but `prompt`) that the dataset may have. For example, if the dataset contains a column named `ground_truth`, the function will be called with `ground_truth` as a keyword argument. The easiest way to comply with this requirement is to use `**kwargs` in the function signature. - Depending on the dataset format, the input will vary: - For [standard format](dataset_formats#standard), `prompts` and `completions` will be lists of strings. - For [conversational format](dataset_formats#conversational), `prompts` and `completions` will be lists of message dictionaries. 2. **Return value**: The function must return a list of floats. Each float represents the reward corresponding to a single completion. #### Example 1: Reward longer completions Below is an example of a reward function for a standard format that rewards longer completions: ```python def reward_func(completion_ids, **kwargs): """Reward function that assigns higher scores to longer completions (in terms of token count).""" return [float(len(ids)) for ids in completion_ids] ``` You can test it as follows: ```python >>> prompts = ["The sky is", "The sun is"] # not used in the reward function, but the trainer will pass it >>> completions = [" blue.", " in the sky."] # not used in the reward function, but the trainer will pass it >>> completion_ids = [[6303, 13], [304, 279, 12884, 13]] >>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids) [2.0, 4.0] ``` #### Example 1.1: Reward longer completions (based on the number of characters) Same as the previous example, but this time the reward function is based on the number of characters instead of tokens. ```python def reward_func(completions, **kwargs): """Reward function that assigns higher scores to longer completions (in terms of character count).""" return [float(len(completion)) for completion in completions] ``` You can test it as follows: ```python >>> prompts = ["The sky is", "The sun is"] >>> completions = [" blue.", " in the sky."] >>> completion_ids = [[6303, 13], [304, 279, 12884, 13]] # not used in the reward function, but the trainer will pass it >>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids) [6.0, 12.0] ``` #### Example 2: Reward completions with a specific format Below is an example of a reward function that checks if the completion has a specific format. This example is inspired by the _format reward_ function used in the paper [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://huggingface.co/papers/2501.12948). It is designed for a conversational format, where prompts and completions consist of structured messages. ```python import re def format_reward_func(completions, **kwargs): """Reward function that checks if the completion has a specific format.""" pattern = r"^.*?.*?$" completion_contents = [completion[0]["content"] for completion in completions] matches = [re.match(pattern, content) for content in completion_contents] return [1.0 if match else 0.0 for match in matches] ``` You can test this function as follows: ```python >>> prompts = [ ... [{"role": "assistant", "content": "What is the result of (1 + 2) * 4?"}], ... [{"role": "assistant", "content": "What is the result of (3 + 1) * 2?"}], ... ] >>> completions = [ ... [{"role": "assistant", "content": "The sum of 1 and 2 is 3, which we multiply by 4 to get 12.(1 + 2) * 4 = 12"}], ... [{"role": "assistant", "content": "The sum of 3 and 1 is 4, which we multiply by 2 to get 8. So (3 + 1) * 2 = 8."}], ... ] >>> format_reward_func(prompts=prompts, completions=completions) [1.0, 0.0] ``` #### Example 3: Reward completions based on a reference Below is an example of a reward function that checks if the completion is correct. This example is inspired by the _accuracy reward_ function used in the paper [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://huggingface.co/papers/2501.12948). This example is designed for [standard format](dataset_formats#standard), where the dataset contains a column named `ground_truth`. ```python import re def reward_func(completions, ground_truth, **kwargs): # Regular expression to capture content inside \boxed{} matches = [re.search(r"\\boxed\{(.*?)\}", completion) for completion in completions] contents = [match.group(1) if match else "" for match in matches] # Reward 1 if the content is the same as the ground truth, 0 otherwise return [1.0 if c == gt else 0.0 for c, gt in zip(contents, ground_truth)] ``` You can test this function as follows: ```python >>> prompts = ["Problem: Solve the equation $2x + 3 = 7$. Solution:", "Problem: Solve the equation $3x - 5 = 10$."] >>> completions = [r" The solution is \boxed{2}.", r" The solution is \boxed{6}."] >>> ground_truth = ["2", "5"] >>> reward_func(prompts=prompts, completions=completions, ground_truth=ground_truth) [1.0, 0.0] ``` #### Example 4: Multi-task reward functions Below is an example of using multiple reward functions in the [`GRPOTrainer`]. In this example, we define two task-specific reward functions: `math_reward_func` and `coding_reward_func`. The `math_reward_func` rewards math problems based on their correctness, while the `coding_reward_func` rewards coding problems based on whether the solution works. ```python from datasets import Dataset from trl import GRPOTrainer # Define a dataset that contains both math and coding problems dataset = Dataset.from_list( [ {"prompt": "What is 2+2?", "task": "math"}, {"prompt": "Write a function that returns the sum of two numbers.", "task": "code"}, {"prompt": "What is 3*4?", "task": "math"}, {"prompt": "Write a function that returns the product of two numbers.", "task": "code"}, ] ) # Math-specific reward function def math_reward_func(prompts, completions, task, **kwargs): rewards = [] for prompt, completion, t in zip(prompts, completions, task): if t == "math": # Calculate math-specific reward correct = check_math_solution(prompt, completion) reward = 1.0 if correct else -1.0 rewards.append(reward) else: # Return None for non-math tasks rewards.append(None) return rewards # Coding-specific reward function def coding_reward_func(prompts, completions, task, **kwargs): rewards = [] for prompt, completion, t in zip(prompts, completions, task): if t == "coding": # Calculate coding-specific reward works = test_code_solution(prompt, completion) reward = 1.0 if works else -1.0 rewards.append(reward) else: # Return None for non-coding tasks rewards.append(None) return rewards # Use both task-specific reward functions trainer = GRPOTrainer( model="Qwen/Qwen2-0.5B-Instruct", reward_funcs=[math_reward_func, coding_reward_func], train_dataset=dataset, ) trainer.train() ``` In this example, the `math_reward_func` and `coding_reward_func` are designed to work with a mixed dataset that contains both math and coding problems. The `task` column in the dataset is used to determine which reward function to apply to each problem. If there is no relevant reward function for a sample in the dataset, the reward function will return `None`, and the [`GRPOTrainer`] will continue with the valid functions and tasks. This allows the [`GRPOTrainer`] to handle multiple reward functions with different applicability. Note that the [`GRPOTrainer`] will ignore the `None` rewards returned by the reward functions and only consider the rewards returned by the relevant functions. This ensures that the model is trained on the relevant tasks and ignores the tasks for which there is no relevant reward function. #### Example 5: Asynchronous reward functions Custom reward functions can also be defined as `async def` coroutines. This is useful if your reward depends on slow I/O (for example, calling a remote service). When you pass multiple async reward functions, [`GRPOTrainer`] executes them concurrently so their latency overlaps. Below is a minimal example of an async reward function that simulates an I/O-bound operation: ```python import asyncio async def async_reward_func(prompts, completions, **kwargs): # Simulate an I/O-bound call (e.g., HTTP request, database lookup) await asyncio.sleep(0.01) # Simple toy reward: 1.0 if the completion is non-empty, else 0.0 return [1.0 if completion else 0.0 for completion in completions] ``` #### Example 6: Logging extra columns and metrics Below is an example of a reward function that logs extra columns to the completions table and scalar metrics as plots. ```python import re def reward_func(completions, ground_truth, log_extra=None, log_metric=None, **kwargs): extracted = [re.search(r"\\boxed\{(.*?)\}", c) for c in completions] extracted = [m.group(1) if m else None for m in extracted] rewards = [1.0 if e == gt else 0.0 for e, gt in zip(extracted, ground_truth)] if log_extra: log_extra("golden_answer", list(ground_truth)) log_extra("extracted_answer", [e or "[none]" for e in extracted]) if log_metric: log_metric("accuracy", sum(rewards) / len(rewards)) return rewards ``` #### Passing the reward function to the trainer To use your custom reward function, pass it to the [`GRPOTrainer`] as follows: ```python from trl import GRPOTrainer trainer = GRPOTrainer( reward_funcs=reward_func, ..., ) ``` You can pass several reward functions as a list; this list may include both synchronous and asynchronous functions: ```python from trl import GRPOTrainer trainer = GRPOTrainer( reward_funcs=[reward_func, async_reward_func1, async_reward_func2], ..., ) ``` and the reward will be computed as the sum of the rewards from each function, or the weighted sum if `reward_weights` is provided in the config. Note that [`GRPOTrainer`] supports multiple reward functions of different types. See the parameters documentation for more details. ### Rapid Experimentation for GRPO RapidFire AI is an open-source experimentation engine that sits on top of TRL and lets you launch multiple GRPO configurations at once, even on a single GPU. Instead of trying configurations sequentially, RapidFire lets you **see all their learning curves earlier, stop underperforming runs, and clone promising ones with new settings in flight** without restarting. For more information, see [RapidFire AI Integration](rapidfire_integration). ## Agent Training GRPO supports **agent training** through the `tools` argument in [`GRPOTrainer`]. This parameter expects a list of Python functions (sync or async) that define the tools available to the agent: ```python from trl import GRPOTrainer trainer = GRPOTrainer( tools=[tool1, tool2], ..., ) ``` Each tool must be a standard Python function with **type-hinted arguments and return types**, along with a **Google-style docstring** describing its purpose, arguments, and return value. For more details, see the [Passing tools guide](https://huggingface.co/docs/transformers/en/chat_extras#passing-tools). Example: ```python from trl import GRPOTrainer def multiply(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b async def async_add(a: int, b: int) -> int: """ Asynchronously adds two integers. Args: a: The first integer. b: The second integer. Returns: The sum of the two integers. """ return a + b trainer = GRPOTrainer( tools=[multiply, async_add], ..., ) ``` You can also provide tools through `environment_factory`. In this mode, [`GRPOTrainer`] creates one environment instance per rollout and exposes the environment's public methods as tools. > [!IMPORTANT] > `environment_factory` requires `transformers>=5.2.0`. The following is a minimal example of using `environment_factory` to define a simple environment with an `increment` method, which is exposed as a tool to the agent: ```python from datasets import Dataset from trl import GRPOConfig, GRPOTrainer instructions = [f"Increment the counter by {i}." for i in range(1, 7)] dataset = Dataset.from_dict({"prompt": [[{"role": "user", "content": instruction}] for instruction in instructions]}) def reward_func(environments, **kwargs): # dummy reward: the reward is the current value of the counter return [environment.counter for environment in environments] class IncrementEnv: def reset(self, **kwargs) -> str | None: # required; receives sampled row fields as kwargs (e.g., `prompt`) self.counter = 0 return "Counter reset to 0.\n" def increment(self, step: int) -> int: # the other public methods of the environment are exposed as tools """ Increment the internal counter. Args: step: Value to add to the counter. Returns: The updated counter value. """ self.counter += step return self.counter trainer = GRPOTrainer( model="Qwen/Qwen3-0.6B", args=GRPOConfig(chat_template_kwargs={"enable_thinking": False}), train_dataset=dataset, reward_funcs=reward_func, environment_factory=IncrementEnv, ) trainer.train() ``` `reset` can return either `None` or a string. In GRPO, when it returns a string, that string is appended to the last user message before generation. ### Supported Models Tested with: - [**Qwen3**](https://huggingface.co/collections/Qwen/qwen3) — e.g., `Qwen/Qwen3-0.6B` - [**Qwen3.5**](https://huggingface.co/collections/Qwen/qwen35) — e.g., `Qwen/Qwen3.5-2B` > [!TIP] > Compatibility with all LLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub — or better yet, submit a pull request with the required changes. ### Quick Start Use [grpo\_agent.py](https://github.com/huggingface/trl/blob/main/examples/scripts/grpo_agent.py) to fine-tune a LLM for agentic workflows. ```bash accelerate launch \ --config_file=examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/grpo_agent.py \ --model_name_or_path Qwen/Qwen3-0.6B ... ``` ## Vision-Language Model (VLM) Training GRPO supports training Vision-Language Models (VLMs) on multimodal datasets containing both text and images. ### Supported Models Tested with: - **Gemma3** — e.g., `google/gemma-3-4b-it` - **LLaVA-NeXT** — e.g., `llava-hf/llava-v1.6-mistral-7b-hf` - **Qwen2-VL** — e.g., `Qwen/Qwen2-VL-2B-Instruct` - **Qwen2.5-VL** — e.g., `Qwen/Qwen2.5-VL-3B-Instruct` - **SmolVLM2** — e.g., `HuggingFaceTB/SmolVLM2-2.2B-Instruct` > [!TIP] > Compatibility with all VLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub — or better yet, submit a pull request with the required changes. ### Quick Start Use [grpo\_vlm.py](https://github.com/huggingface/trl/blob/main/examples/scripts/grpo_vlm.py) to fine-tune a VLM. Example command for training on [`lmms-lab/multimodal-open-r1-8k-verified`](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified): ```bash accelerate launch \ --config_file=examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/grpo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir grpo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_vllm \ --vllm_mode colocate \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions ``` ### Configuration Tips - Use LoRA on vision-language projection layers - Enable 4-bit quantization to reduce memory usage - VLMs are memory-intensive — start with smaller batch sizes - Most models are compatible with vLLM (`server` and `colocate` modes) ### Dataset Format Each training sample should include: - `prompt`: Text formatted via the processor's chat template - `image`/`images`: PIL Image or list of PIL Images The trainer automatically handles image-to-tensor conversion via the model’s image processor. ## GRPOTrainer [[autodoc]] GRPOTrainer - train - save_model - push_to_hub ## GRPOConfig [[autodoc]] GRPOConfig ================================================ FILE: docs/source/grpo_with_replay_buffer.md ================================================ # GRPO With Replay Buffer This experimental trainer, trains a model with GRPO but replaces groups (and corresponding completions) that have 0 standard deviation with groups with high rewards and standard deviation that've been used to train a model in prior batches. ## Usage ```python import torch from trl.experimental.grpo_with_replay_buffer import GRPOWithReplayBufferConfig, GRPOWithReplayBufferTrainer from datasets import load_dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Guarantee that some rewards have 0 std def custom_reward_func(completions, **kwargs): if torch.rand(1).item() < 0.25: return [0] * len(completions) # simulate some None rewards else: return torch.rand(len(completions)).tolist() training_args = GRPOWithReplayBufferConfig( output_dir="./tmp", learning_rate=1e-4, per_device_train_batch_size=4, num_generations=4, max_completion_length=8, replay_buffer_size=8, report_to="none", ) trainer = GRPOWithReplayBufferTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[custom_reward_func], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() ``` ## GRPOWithReplayBufferTrainer [[autodoc]] experimental.grpo_with_replay_buffer.GRPOWithReplayBufferTrainer - train - save_model - push_to_hub ## GRPOWithReplayBufferConfig [[autodoc]] experimental.grpo_with_replay_buffer.GRPOWithReplayBufferConfig ## ReplayBuffer [[autodoc]] experimental.grpo_with_replay_buffer.ReplayBuffer ================================================ FILE: docs/source/gspo_token.md ================================================ # GSPO-token In the paper [Group Sequence Policy Optimization](https://huggingface.co/papers/2507.18071), the authors propose a token-level objective variant to GSPO, called GSPO-token. To use GSPO-token, you can use the `GRPOTrainer` class in `trl.experimental.gspo_token`. ## Usage ```python from trl.experimental.gspo_token import GRPOTrainer from trl import GRPOConfig training_args = GRPOConfig( importance_sampling_level="sequence_token", ... ) ``` > [!WARNING] > To leverage GSPO-token, the user will need to provide the per-token advantage \\( \hat{A_{i,t}} \\) for each token \\( t \\) in the sequence \\( i \\) (i.e., make \\( \hat{A_{i,t}} \\) varies with \\( t \\)—which isn't the case here, \\( \hat{A_{i,t}}=\hat{A_{i}} \\)). Otherwise, GSPO-Token gradient is just equivalent to the original GSPO implementation. ## GRPOTrainer [[autodoc]] experimental.gspo_token.GRPOTrainer - train - save_model - push_to_hub ================================================ FILE: docs/source/index.md ================================================
# TRL - Transformers Reinforcement Learning TRL is a full stack library where we provide a set of tools to train transformer language models with methods like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), Direct Preference Optimization (DPO), Reward Modeling, and more. The library is integrated with 🤗 [transformers](https://github.com/huggingface/transformers). ## 🎉 What's New **OpenEnv Integration:** TRL now supports **[OpenEnv](https://huggingface.co/blog/openenv)**, the open-source framework from Meta for defining, deploying, and interacting with environments in reinforcement learning and agentic workflows. Explore how to seamlessly integrate TRL with OpenEnv in our [dedicated documentation](openenv). ## Taxonomy Below is the current list of TRL trainers, organized by method type (⚡️ = vLLM support; 🧪 = experimental).
### Online methods - [`GRPOTrainer`](grpo_trainer) ⚡️ - [`RLOOTrainer`](rloo_trainer) ⚡️ - [`OnlineDPOTrainer`](online_dpo_trainer) 🧪 ⚡️ - [`NashMDTrainer`](nash_md_trainer) 🧪 ⚡️ - [`PPOTrainer`](ppo_trainer) 🧪 - [`XPOTrainer`](xpo_trainer) 🧪 ⚡️ ### Reward modeling - [`RewardTrainer`](reward_trainer) - [`PRMTrainer`](prm_trainer) 🧪
### Offline methods - [`SFTTrainer`](sft_trainer) - [`DPOTrainer`](dpo_trainer) - [`BCOTrainer`](bco_trainer) 🧪 - [`CPOTrainer`](cpo_trainer) 🧪 - [`KTOTrainer`](kto_trainer) 🧪 - [`ORPOTrainer`](orpo_trainer) 🧪 ### Knowledge distillation - [`GKDTrainer`](gkd_trainer) 🧪 - [`MiniLLMTrainer`](minillm_trainer) 🧪
You can also explore TRL-related models, datasets, and demos in the [TRL Hugging Face organization](https://huggingface.co/trl-lib). ## Learn Learn post-training with TRL and other libraries in 🤗 [smol course](https://github.com/huggingface/smol-course). ## Contents The documentation is organized into the following sections: - **Getting Started**: installation and quickstart guide. - **Conceptual Guides**: dataset formats, training FAQ, and understanding logs. - **How-to Guides**: reducing memory usage, speeding up training, distributing training, etc. - **Integrations**: DeepSpeed, Liger Kernel, PEFT, etc. - **Examples**: example overview, community tutorials, etc. - **API**: trainers, utils, etc. ## Blog posts ## Talks ================================================ FILE: docs/source/installation.md ================================================ # Installation You can install TRL either from PyPI or from source: ## PyPI Install the library with pip or [uv](https://docs.astral.sh/uv/): uv is a fast Rust-based Python package and project manager. Refer to [Installation](https://docs.astral.sh/uv/getting-started/installation/) for installation instructions. ```bash uv pip install trl ``` ```bash pip install trl ``` ## Source You can also install the latest version from source. First clone the repo and then run the installation with `pip`: ```bash git clone https://github.com/huggingface/trl.git cd trl/ pip install -e . ``` If you want the development install you can replace the pip install with the following: ```bash pip install -e ".[dev]" ``` ================================================ FILE: docs/source/jobs_training.md ================================================ # Training with Jobs [![model badge](https://img.shields.io/badge/All_models-HF_Jobs-blue)](https://huggingface.co/models?other=hf_jobs,trl) [Hugging Face Jobs](https://huggingface.co/docs/huggingface_hub/guides/jobs) lets you run training scripts on fully managed infrastructure—no need to manage GPUs or local environment setup. In this guide, you'll learn how to: * Use [TRL Jobs](https://github.com/huggingface/trl-jobs) to easily run pre-optimized TRL training * Run any TRL training script with uv scripts For general details about Hugging Face Jobs (hardware selection, job monitoring, etc.), see the [Jobs documentation](https://huggingface.co/docs/huggingface_hub/guides/jobs). ## Requirements * A [Pro](https://hf.co/pro), [Team](https://hf.co/enterprise), or [Enterprise](https://hf.co/enterprise) plan * Logged in to the Hugging Face Hub (`hf auth login`) ## Using TRL Jobs [TRL Jobs](https://github.com/huggingface/trl-jobs) is a high-level wrapper around Hugging Face Jobs and TRL that streamlines training. It provides optimized default configurations so you can start quickly without manually tuning parameters. Example: ```bash pip install trl-jobs trl-jobs sft --model_name Qwen/Qwen3-0.6B --dataset_name trl-lib/Capybara ``` TRL Jobs supports everything covered in this guide, with additional optimizations to simplify workflows. ## Using uv Scripts For more control, you can run Hugging Face Jobs directly with your own scripts, using [uv scripts](https://docs.astral.sh/uv/guides/scripts/). Create a Python script (e.g., `train.py`) containing your training code: ```python from datasets import load_dataset from trl import SFTTrainer dataset = load_dataset("trl-lib/Capybara", split="train") trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B", train_dataset=dataset, ) trainer.train() trainer.push_to_hub("Qwen2.5-0.5B-SFT") ``` Launch the job using either the [`hf jobs` CLI](https://huggingface.co/docs/huggingface_hub/guides/cli#hf-jobs) or the Python API: ```bash hf jobs uv run \ --flavor a100-large \ --with trl \ --secrets HF_TOKEN \ train.py ``` ```python from huggingface_hub import run_uv_job run_uv_job( "train.py", dependencies=["trl"], flavor="a100-large", secrets={"HF_TOKEN": "hf_..."}, ) ``` To run successfully, the script needs: * **TRL installed**: Use the `--with trl` flag or the `dependencies` argument. uv installs these dependencies automatically before running the script. * **An authentication token**: Required to push the trained model (or perform other authenticated operations). Provide it with the `--secrets HF_TOKEN` flag or the `secrets` argument. > [!WARNING] > When training with Jobs, be sure to: > > * **Set a sufficient timeout**. Jobs time out after 30 minutes by default. If your job exceeds the timeout, it will fail and all progress will be lost. See [Setting a custom timeout](https://huggingface.co/docs/huggingface_hub/guides/jobs#setting-a-custom-timeout). > * **Push the model to the Hub**. The Jobs environment is ephemeral—files are deleted when the job ends. If you don’t push the model, it will be lost. You can also run a script directly from a URL: ```bash hf jobs uv run \ --flavor a100-large \ --with trl \ --secrets HF_TOKEN \ "https://gist.githubusercontent.com/qgallouedec/eb6a7d20bd7d56f9c440c3c8c56d2307/raw/69fd78a179e19af115e4a54a1cdedd2a6c237f2f/train.py" ``` ```python from huggingface_hub import run_uv_job run_uv_job( "https://gist.githubusercontent.com/qgallouedec/eb6a7d20bd7d56f9c440c3c8c56d2307/raw/69fd78a179e19af115e4a54a1cdedd2a6c237f2f/train.py", flavor="a100-large", dependencies=["trl"], secrets={"HF_TOKEN": "hf_..."}, ) ``` To make a script self-contained, declare dependencies at the top: ```python # /// script # dependencies = [ # "trl", # "peft", # ] # /// from datasets import load_dataset from peft import LoraConfig from trl import SFTTrainer dataset = load_dataset("trl-lib/Capybara", split="train") trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B", train_dataset=dataset, peft_config=LoraConfig(), ) trainer.train() trainer.push_to_hub("Qwen2.5-0.5B-SFT") ``` You can then run the script without specifying dependencies: ```bash hf jobs uv run \ --flavor a100-large \ --secrets HF_TOKEN \ train.py ``` ```python from huggingface_hub import run_uv_job run_uv_job( "train.py", flavor="a100-large", secrets={"HF_TOKEN": "hf_..."}, ) ``` TRL example scripts are fully uv-compatible, so you can run a complete training workflow directly on Jobs. You can customize training with standard script arguments plus hardware and secrets: ```bash hf jobs uv run \ --flavor a100-large \ --secrets HF_TOKEN \ https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/examples/scripts/prm.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/prm800k \ --output_dir Qwen2-0.5B-Reward \ --push_to_hub ``` ```python from huggingface_hub import run_uv_job run_uv_job( "https://raw.githubusercontent.com/huggingface/trl/refs/heads/main/examples/scripts/prm.py", flavor="a100-large", secrets={"HF_TOKEN": "hf_..."}, script_args=[ "--model_name_or_path", "Qwen/Qwen2-0.5B-Instruct", "--dataset_name", "trl-lib/prm800k", "--output_dir", "Qwen2-0.5B-Reward", "--push_to_hub" ] ) ``` See the full list of examples in [Maintained examples](example_overview#maintained-examples). ### Docker Images An up-to-date Docker image with all TRL dependencies is available at [huggingface/trl](https://hub.docker.com/r/huggingface/trl) and can be used directly with Hugging Face Jobs: ```bash hf jobs uv run \ --flavor a100-large \ --secrets HF_TOKEN \ --image huggingface/trl \ train.py ``` ```python from huggingface_hub import run_uv_job run_uv_job( "train.py", flavor="a100-large", secrets={"HF_TOKEN": "hf_..."}, image="huggingface/trl", ) ``` Jobs runs on a Docker image from Hugging Face Spaces or Docker Hub, so you can also specify any custom image: ```bash hf jobs uv run \ --flavor a100-large \ --secrets HF_TOKEN \ --image \ --secrets HF_TOKEN \ train.py ``` ```python from huggingface_hub import run_uv_job run_uv_job( "train.py", flavor="a100-large", secrets={"HF_TOKEN": "hf_..."}, image="", ) ``` ================================================ FILE: docs/source/judges.md ================================================ # Judges > [!WARNING] > TRL Judges is an experimental API which is subject to change at any time. As of TRL v1.0, judges have been moved to the `trl.experimental.judges` module. TRL provides judges to easily compare two completions. Make sure to have installed the required dependencies by running: ```bash pip install trl[judges] ``` ## Using the provided judges TRL provides several judges out of the box. For example, you can use the [`experimental.judges.HfPairwiseJudge`] to compare two completions using a pre-trained model from the Hugging Face model hub: ```python from trl.experimental.judges import HfPairwiseJudge judge = HfPairwiseJudge() judge.judge( prompts=["What is the capital of France?", "What is the biggest planet in the solar system?"], completions=[["Paris", "Lyon"], ["Saturn", "Jupiter"]], ) # Outputs: [0, 1] ``` ## Define your own judge To define your own judge, we provide several base classes that you can subclass. For rank-based judges, you need to subclass [`experimental.judges.BaseRankJudge`] and implement the [`experimental.judges.BaseRankJudge.judge`] method. For pairwise judges, you need to subclass [`experimental.judges.BasePairJudge`] and implement the [`experimental.judges.BasePairJudge.judge`] method. If you want to define a judge that doesn't fit into these categories, you need to subclass [`experimental.judges.BaseJudge`] and implement the [`experimental.judges.BaseJudge.judge`] method. As an example, let's define a pairwise judge that prefers shorter completions: ```python from trl.experimental.judges import BasePairwiseJudge class PrefersShorterJudge(BasePairwiseJudge): def judge(self, prompts, completions, shuffle_order=False): return [0 if len(completion[0]) > len(completion[1]) else 1 for completion in completions] ``` You can then use this judge as follows: ```python judge = PrefersShorterJudge() judge.judge( prompts=["What is the capital of France?", "What is the biggest planet in the solar system?"], completions=[["Paris", "The capital of France is Paris."], ["Jupiter is the biggest planet in the solar system.", "Jupiter"]], ) # Outputs: [0, 1] ``` ## Provided judges ### PairRMJudge [[autodoc]] experimental.judges.PairRMJudge ### HfPairwiseJudge [[autodoc]] experimental.judges.HfPairwiseJudge ### OpenAIPairwiseJudge [[autodoc]] experimental.judges.OpenAIPairwiseJudge ### AllTrueJudge [[autodoc]] experimental.judges.AllTrueJudge ## Base classes ### BaseJudge [[autodoc]] experimental.judges.BaseJudge ### BaseBinaryJudge [[autodoc]] experimental.judges.BaseBinaryJudge ### BaseRankJudge [[autodoc]] experimental.judges.BaseRankJudge ### BasePairwiseJudge [[autodoc]] experimental.judges.BasePairwiseJudge ================================================ FILE: docs/source/kernels_hub.md ================================================ # Kernels Hub Integration and Usage kernel-builder logo The [`kernels`](https://huggingface.co/blog/hello-hf-kernels#get-started-and-next-steps) library allows optimized compute kernels to be loaded directly from the Hub. You can find `kernels` in [dedicated orgs](https://huggingface.co/kernels-community) or by searching for the [`kernel` tag](https://huggingface.co/models?other=kernel) within the Hub. Kernels are **optimized code pieces** that help in model development, training, and inference. Here, we’ll focus on their **integration with TRL**, but check out the above resources to learn more about them. ## Installation To use kernels with TRL, you'd need to install the library in your Python environment: ```bash pip install kernels ``` ## Using Kernels from the Hub in TRL Kernels can directly replace attention implementations, removing the need to manually compile attention backends like Flash Attention and boosting training speed just by pulling the respective attention kernel from the Hub. You can specify a kernel when loading a model: ```python from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained( "your-model-name", attn_implementation="kernels-community/flash-attn2" # other options: kernels-community/vllm-flash-attn3, kernels-community/paged-attention ) ``` Or when running a TRL training script: ```bash python sft.py ... --attn_implementation kernels-community/flash-attn2 ``` Or using the TRL CLI: ```bash trl sft ... --attn_implementation kernels-community/flash-attn2 ``` > [!TIP] > Now you can leverage faster attention backends with a pre-optimized kernel for your hardware configuration from the Hub, speeding up both development and training. ## Comparing Attention Implementations We evaluated various attention implementations available in transformers, along with different kernel backends, using **TRL** and **SFT**. The experiments were run on a single **H100 GPU** with **CUDA 12.9**, leveraging **Qwen3-8B** with a **batch size of 8**, **gradient accumulation of 1**, and **bfloat16** precision. Keep in mind that the results shown here are specific to this setup and may vary with different training configurations. The following figure illustrates both **latency** (time per training step) and **peak allocated memory** for the different attention implementations and kernel backends. Kernel-based implementations perform on par with custom-installed attention, and increasing the model’s `max_length` further enhances performance. Memory consumption is similar across all implementations, showing no significant differences. We get the same performance but with less friction, as described in [the following section](#flash-attention-vs-hub-kernels).
Latency and Memory Usage Latency and Memory Usage
## Flash Attention vs. Hub Kernels Building Flash Attention from source can be time-consuming, often taking anywhere from several minutes to hours, depending on your hardware, CUDA/PyTorch configuration, and whether precompiled wheels are available. In contrast, **Hugging Face Kernels** provide a much faster and more reliable workflow. Developers don’t need to worry about complex setups—everything is handled automatically. In our benchmarks, kernels were ready to use in about **2.5 seconds**, with no compilation required. This allows you to start training almost instantly, significantly accelerating development. Simply specify the desired version, and `kernels` takes care of the rest. ## Combining FlashAttention Kernels with Liger Kernels You can combine **FlashAttention kernels** with **Liger kernels** for additional TRL performance improvements. First, install the Liger kernel dependency: ```bash pip install liger-kernel ``` Then, combine both in your code: ```python from transformers import AutoModelForCausalLM from trl import SFTConfig model = AutoModelForCausalLM.from_pretrained( "your-model-name", attn_implementation="kernels-community/flash-attn2" # choose the desired FlashAttention variant ) training_args = SFTConfig( use_liger_kernel=True, # ... other TRL training args ) ``` Learn more about the [Liger Kernel Integration](./liger_kernel_integration). ================================================ FILE: docs/source/kto_trainer.md ================================================ # KTO Trainer [![model badge](https://img.shields.io/badge/All_models-KTO-blue)](https://huggingface.co/models?other=kto,trl) > [!WARNING] > As of TRL v1.0, `KTOTrainer` and `KTOConfig` have been moved to the `trl.experimental.kto` module. > KTO API is experimental and may change at any time. > Promoting KTO back into the stable API is a high-priority task: KTO is slated for refactoring to align with the standard core trainer architecture. ## Overview Kahneman-Tversky Optimization (KTO) was introduced in [KTO: Model Alignment as Prospect Theoretic Optimization](https://huggingface.co/papers/2402.01306) by [Kawin Ethayarajh](https://huggingface.co/kawine), [Winnie Xu](https://huggingface.co/xwinxu), [Niklas Muennighoff](https://huggingface.co/Muennighoff), Dan Jurafsky, [Douwe Kiela](https://huggingface.co/douwekiela). The abstract from the paper is the following: > Kahneman & Tversky's prospect theory tells us that humans perceive random variables in a biased but well-defined manner; for example, humans are famously loss-averse. We show that objectives for aligning LLMs with human feedback implicitly incorporate many of these biases -- the success of these objectives (e.g., DPO) over cross-entropy minimization can partly be ascribed to them being human-aware loss functions (HALOs). However, the utility functions these methods attribute to humans still differ from those in the prospect theory literature. Using a Kahneman-Tversky model of human utility, we propose a HALO that directly maximizes the utility of generations instead of maximizing the log-likelihood of preferences, as current methods do. We call this approach Kahneman-Tversky Optimization (KTO), and it matches or exceeds the performance of preference-based methods at scales from 1B to 30B. Crucially, KTO does not need preferences -- only a binary signal of whether an output is desirable or undesirable for a given input. This makes it far easier to use in the real world, where preference data is scarce and expensive. The official code can be found in [ContextualAI/HALOs](https://github.com/ContextualAI/HALOs). This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif), [Younes Belkada](https://huggingface.co/ybelkada), [Lewis Tunstall](https://huggingface.co/lewtun) and Pablo Vicente. ## Quick start This example demonstrates how to train a model using the KTO method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model. We use the preference data from the [KTO Mix 14k](https://huggingface.co/datasets/trl-lib/kto-mix-14k). You can view the data in the dataset here: Below is the script to train the model: ```python # train_kto.py from datasets import load_dataset from trl.experimental.kto import KTOConfig, KTOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") train_dataset = load_dataset("trl-lib/kto-mix-14k", split="train") training_args = KTOConfig(output_dir="Qwen2-0.5B-KTO") trainer = KTOTrainer(model=model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_kto.py ``` Distributed across 8 x H100 GPUs, the training takes approximately 30 minutes. You can verify the training progress by checking the reward graph. An increasing trend in the reward margin indicates that the model is improving and generating better responses over time. ![kto qwen2 reward margin](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/kto-qwen2-reward-margin.png) To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-KTO) performs, you can use the [Transformers Chat CLI](https://huggingface.co/docs/transformers/quicktour#chat-with-text-generation-models).
$ transformers chat trl-lib/Qwen2-0.5B-KTO
<quentin_gallouedec>:
What is the best programming language?

<trl-lib/Qwen2-0.5B-KTO>:
The best programming language can vary depending on individual preferences, industry-specific requirements, technical skills, and familiarity with the specific use case or task. Here are some widely-used programming languages that have been noted as popular and widely used:

Here are some other factors to consider when choosing a programming language for a project:

 1 JavaScript: JavaScript is at the heart of the web and can be used for building web applications, APIs, and interactive front-end applications like frameworks like React and Angular. It's similar to C, C++, and F# in syntax structure and is accessible and easy to learn, making it a popular choice for beginners and professionals alike.
 2 Java: Known for its object-oriented programming (OOP) and support for Java 8 and .NET, Java is used for developing enterprise-level software applications, high-performance games, as well as mobile apps, game development, and desktop applications.
 3 C++: Known for its flexibility and scalability, C++ offers comprehensive object-oriented programming and is a popular choice for high-performance computing and other technical fields. It's a powerful platform for building real-world applications and games at scale.
 4 Python: Developed by Guido van Rossum in 1991, Python is a high-level, interpreted, and dynamically typed language known for its simplicity, readability, and versatility.
## Expected dataset format KTO requires an [unpaired preference dataset](dataset_formats#unpaired-preference). Alternatively, you can provide a *paired* preference dataset (also known simply as a *preference dataset*). In this case, the trainer will automatically convert it to an unpaired format by separating the chosen and rejected responses, assigning `label = True` to the chosen completions and `label = False` to the rejected ones. The [`experimental.kto.KTOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. In theory, the dataset should contain at least one chosen and one rejected completion. However, some users have successfully run KTO using *only* chosen or only rejected data. If using only rejected data, it is advisable to adopt a conservative learning rate. ## Example script We provide an example script to train a model using the KTO method. The script is available in [`trl/scripts/kto.py`](https://github.com/huggingface/trl/blob/main/trl/scripts/kto.py) To test the KTO script with the [Qwen2 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/trl-lib/kto-mix-14k), run the following command: ```bash accelerate launch trl/scripts/kto.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/kto-mix-14k \ --num_train_epochs 1 \ --output_dir Qwen2-0.5B-KTO ``` ## Usage tips ### For Mixture of Experts Models: Enabling the auxiliary loss MOEs are the most efficient if the load is about equally distributed between experts. To ensure that we train MOEs similarly during preference-tuning, it is beneficial to add the auxiliary loss from the load balancer to the final loss. This option is enabled by setting `output_router_logits=True` in the model config (e.g. [`~transformers.MixtralConfig`]). To scale how much the auxiliary loss contributes to the total loss, use the hyperparameter `router_aux_loss_coef=...` (default: `0.001`) in the model config. ### Batch size recommendations Use a per-step batch size that is at least 4, and an effective batch size between 16 and 128. Even if your effective batch size is large, if your per-step batch size is poor, then the KL estimate in KTO will be poor. ### Learning rate recommendations Each choice of `beta` has a maximum learning rate it can tolerate before learning performance degrades. For the default setting of `beta = 0.1`, the learning rate should typically not exceed `1e-6` for most models. As `beta` decreases, the learning rate should also be reduced accordingly. In general, we strongly recommend keeping the learning rate between `5e-7` and `5e-6`. Even with small datasets, we advise against using a learning rate outside this range. Instead, opt for more epochs to achieve better results. ### Imbalanced data The `desirable_weight` and `undesirable_weight` of the [`experimental.kto.KTOConfig`] refer to the weights placed on the losses for desirable/positive and undesirable/negative examples. By default, they are both 1. However, if you have more of one or the other, then you should upweight the less common type such that the ratio of (`desirable_weight` \\(\times\\) number of positives) to (`undesirable_weight` \\(\times\\) number of negatives) is in the range 1:1 to 4:3. ## Logged metrics While training and evaluating, we record the following reward metrics: - `rewards/chosen_sum`: the sum of log probabilities of the policy model for the chosen responses scaled by beta - `rewards/rejected_sum`: the sum of log probabilities of the policy model for the rejected responses scaled by beta - `logps/chosen_sum`: the sum of log probabilities of the chosen completions - `logps/rejected_sum`: the sum of log probabilities of the rejected completions - `logits/chosen_sum`: the sum of logits of the chosen completions - `logits/rejected_sum`: the sum of logits of the rejected completions - `count/chosen`: the count of chosen samples in a batch - `count/rejected`: the count of rejected samples in a batch ## KTOTrainer [[autodoc]] experimental.kto.KTOTrainer - train - save_model - push_to_hub ## KTOConfig [[autodoc]] experimental.kto.KTOConfig ================================================ FILE: docs/source/liger_kernel_integration.md ================================================ # Liger Kernel Integration [Liger Kernel](https://github.com/linkedin/Liger-Kernel) is a collection of Triton kernels designed specifically for LLM training. It can effectively increase multi-GPU training throughput by 20% and reduce memory usage by 60%. That way, we can **4x** our context length, as described in the benchmark below. They have implemented Hugging Face compatible `RMSNorm`, `RoPE`, `SwiGLU`, `CrossEntropy`, `FusedLinearCrossEntropy`, with more to come. The kernel works out of the box with [FlashAttention](https://github.com/Dao-AILab/flash-attention), [PyTorch FSDP](https://pytorch.org/tutorials/intermediate/FSDP_tutorial.html), and [Microsoft DeepSpeed](https://github.com/microsoft/DeepSpeed). With this memory reduction, you can potentially turn off `cpu_offloading` or gradient checkpointing to further boost the performance. | Speed Up | Memory Reduction | | --- | --- | | ![Speed up](https://raw.githubusercontent.com/linkedin/Liger-Kernel/main/docs/images/e2e-tps.png) | ![Memory](https://raw.githubusercontent.com/linkedin/Liger-Kernel/main/docs/images/e2e-memory.png) | ## Supported Trainers Liger Kernel is supported in the following TRL trainers: - **SFT** (Supervised Fine-Tuning) - **DPO** (Direct Preference Optimization) - **GRPO** (Group Relative Policy Optimization) - **KTO** (Kahneman-Tversky Optimization) - **GKD** (Generalized Knowledge Distillation) ## Usage 1. First, install Liger Kernel: ```bash pip install liger-kernel ``` 2. Once installed, set `use_liger_kernel=True` in your trainer config. No other changes are needed! ```python from trl import SFTConfig training_args = SFTConfig(..., use_liger_kernel=True) ``` ```python from trl import DPOConfig training_args = DPOConfig(..., use_liger_kernel=True) ``` ```python from trl import GRPOConfig training_args = GRPOConfig(..., use_liger_kernel=True) ``` ```python from trl import KTOConfig training_args = KTOConfig(..., use_liger_kernel=True) ``` ```python from trl.experimental.gkd import GKDConfig training_args = GKDConfig(..., use_liger_kernel=True) ``` To learn more about Liger-Kernel, visit their [official repository](https://github.com/linkedin/Liger-Kernel/). ================================================ FILE: docs/source/lora_without_regret.md ================================================ # LoRA Without Regret Recent research from the team at [Thinking Machines Lab](https://thinkingmachines.ai/blog/lora/) (Schulman et al., 2025) shows that **LoRA can match full fine-tuning performance** when configured correctly, while using only ~67% of the compute. These findings are exciting to TRL users because they're straightforward to implement and can improve model performance on smaller budgets. This guide provides simple instructions to reproduce the results of the blog post in TRL. > [!TIP] > It is recommended to read the blog post before following this guide, or to consult both resources in parallel for best results. ## Benefits of LoRA over full fine-tuning First of all, let's remind ourselves of the benefits of [LoRA over full fine-tuning](https://huggingface.co/docs/trl/en/peft_integration). LoRA adds adapter layers on top of the base model, which contains significantly fewer parameters than the base model itself. This design reduces GPU memory requirements and enables more efficient training. As described in the [blog](https://thinkingmachines.ai/blog/lora/), this approach was originally thought to involve a performance trade-off, although careful configuration can overcome this trade-off and match full fine-tuning performance. ## Examples with TRL Let's implement and train LoRA adapters in TRL scripts based on the core findings of the blog post. Afterwards, we'll revisit each finding in light of the TRL results. ### Supervised Fine-Tuning (SFT) The blog post performs SFT on a range of models and datasets from the Hub, which we can reproduce in TRL. | Model | Dataset | | --- | --- | | [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B) | [allenai/tulu-3-sft-mixture](https://huggingface.co/datasets/allenai/tulu-3-sft-mixture) | | [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B) | [open-thoughts/OpenThoughts-114k](https://huggingface.co/datasets/open-thoughts/OpenThoughts-114k) | | [Llama-3.1-8B-Instruct](https://huggingface.co/meta-llama/Llama-3.1-8B) | [allenai/tulu-3-sft-mixture](https://huggingface.co/datasets/allenai/tulu-3-sft-mixture) | | [Llama-3.1-8B-Instruct](https://huggingface.co/meta-llama/Llama-3.1-8B) | [open-thoughts/OpenThoughts-114k](https://huggingface.co/datasets/open-thoughts/OpenThoughts-114k) | We can integrate these findings with the TRL Python API like so: ```python from datasets import load_dataset from peft import LoraConfig from trl import SFTTrainer, SFTConfig dataset = load_dataset("open-thoughts/OpenThoughts-114k", split="train") peft_config = LoraConfig(r=256, lora_alpha=16, target_modules="all-linear") training_args = SFTConfig( learning_rate=2e-4, per_device_train_batch_size=1, gradient_accumulation_steps=4, num_train_epochs=1, report_to=["trackio"], ) trainer = SFTTrainer( model="Qwen/Qwen2.5-3B-Instruct", train_dataset=dataset, peft_config=peft_config, args=training_args, ) trainer.train() ``` ```bash hf jobs uv run \ --flavor a100-large \ --timeout 8h \ --secrets HF_TOKEN \ "https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py" \ --model_name_or_path Qwen/Qwen2.5-3B-Instruct \ --dataset_name open-thoughts/OpenThoughts-114k \ --learning_rate 2.0e-5 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 16 \ --use_peft \ --lora_r 256 \ --lora_alpha 16 \ --lora_target_modules all-linear \ --output_dir Qwen2.5-3B-OpenThoughts-LoRA \ --report_to trackio \ --push_to_hub ``` To use Hugging Face Jobs, you will need to be logged in to the Hugging Face Hub (`hf auth login`) and have a [Pro](https://hf.co/pro), [Team](https://hf.co/enterprise), or [Enterprise](https://hf.co/enterprise) plan. Check out the [Jobs documentation](https://huggingface.co/docs/huggingface_hub/en/guides/jobs) for more details. ```bash uv run "https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py" \ --model_name_or_path Qwen/Qwen2.5-3B-Instruct \ --dataset_name open-thoughts/OpenThoughts-114k \ --learning_rate 2.0e-5 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 16 \ --eval_strategy no \ --use_peft \ --lora_r 256 \ --lora_alpha 16 \ --lora_target_modules all-linear \ --output_dir Qwen2.5-3B-OpenThoughts-LoRA \ --report_to trackio \ --push_to_hub ``` To run the script locally, you will need to have `uv` installed. Check out the [uv documentation](https://docs.astral.sh/uv/) for more details. Once training starts, you can monitor the progress in [Trackio](https://huggingface.co/trackio), which will log the URL. ### Reinforcement Learning (GRPO) The blog post performs GRPO on a range of models and datasets from the Hub, and once again we can reproduce the results in TRL. | Model | Dataset | | --- | --- | | [Llama-3.1-8B-Base](https://huggingface.co/meta-llama/Llama-3.2-1B) | [GSM8k](https://huggingface.co/datasets/openai/gsm8k) | | [Llama-3.1-8B-Base](https://huggingface.co/meta-llama/Llama-3.2-1B) | [DeepMath-103K](https://huggingface.co/datasets/zwhe99/DeepMath-103K) | | [Qwen3-8b-base](https://huggingface.co/Qwen/Qwen3-8b-base) | [DeepMath-103K](https://huggingface.co/datasets/zwhe99/DeepMath-103K) | For reinforcement learning, the blog uses a math reasoning task that we can reproduce as a Python function. We can implement these recommendations with the TRL Python API like so: ```python from datasets import load_dataset from peft import LoraConfig from trl import GRPOConfig, GRPOTrainer from trl.rewards import reasoning_accuracy_reward dataset = load_dataset("HuggingFaceH4/OpenR1-Math-220k-default-verified", split="train") peft_config = LoraConfig( r=1, lora_alpha=32, target_modules="all-linear" ) training_args = GRPOConfig( learning_rate=5e-5, per_device_train_batch_size=1, gradient_accumulation_steps=4, num_train_epochs=1, num_generations=8, generation_batch_size=8, report_to=["trackio"], ) trainer = GRPOTrainer( model="Qwen/Qwen3-0.6B", reward_funcs=reasoning_accuracy_reward, args=training_args, train_dataset=dataset, peft_config=peft_config, ) trainer.train() ``` > [!WARNING] > This snippet skips the reward function which is defined above to keep the example concise. ```bash hf jobs uv run \ --flavor a100-large \ --timeout 4h \ --secrets HF_TOKEN \ --env PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ "https://huggingface.co/datasets/burtenshaw/lora-without-regrets/resolve/main/grpo.py" \ --model_name_or_path Qwen/Qwen3-0.6B \ --dataset_name HuggingFaceH4/OpenR1-Math-220k-default-verified \ --output_dir grpo-full-qwen3-0.6b \ --learning_rate 1.0e-6 \ --lr_scheduler_type cosine \ --warmup_steps 0.0 \ --max_grad_norm 1.0 \ --beta 0.0 \ --max_completion_length 4096 \ --num_generations 16 \ --generation_batch_size 16 \ --gradient_accumulation_steps 8 \ --per_device_train_batch_size 1 \ --num_train_epochs 1 \ --lora_r 1 \ --lora_alpha 32 \ --lora_dropout 0.0 \ --lora_target_modules all-linear \ --vllm_mode colocate \ --save_strategy steps \ --save_steps 50 \ --save_total_limit 1 \ --logging_steps 1 \ --max_steps 200 \ --report_to trackio ``` To use Hugging Face Jobs, you will need to be logged in to the Hugging Face Hub (`hf auth login`) and have a [Pro](https://hf.co/pro), [Team](https://hf.co/enterprise), or [Enterprise](https://hf.co/enterprise) plan. Check out the [Jobs documentation](https://huggingface.co/docs/huggingface_hub/en/guides/jobs) for more details. ```bash uv run "https://huggingface.co/datasets/burtenshaw/lora-without-regrets/resolve/main/grpo.py" \ --model_name_or_path Qwen/Qwen3-0.6B \ --dataset_name HuggingFaceH4/OpenR1-Math-220k-default-verified \ --output_dir grpo-full-qwen3-0.6b \ --learning_rate 1.0e-6 \ --lr_scheduler_type cosine \ --warmup_steps 0.0 \ --max_grad_norm 1.0 \ --beta 0.0 \ --max_completion_length 4096 \ --num_generations 16 \ --generation_batch_size 16 \ --gradient_accumulation_steps 8 \ --per_device_train_batch_size 1 \ --num_train_epochs 1 \ --lora_r 1 \ --lora_alpha 32 \ --lora_dropout 0.0 \ --lora_target_modules all-linear \ --vllm_mode colocate \ --save_strategy steps \ --save_steps 50 \ --save_total_limit 1 \ --logging_steps 1 \ --max_steps 200 \ --report_to trackio ``` To run the script locally, you will need to have `uv` installed. Check out the [uv documentation](https://docs.astral.sh/uv/) for more details. The reinforcement learning script with GRPO is implemented as a custom script in TRL, which uses the reward function shown above. You can review it at [`grpo.py`](https://huggingface.co/datasets/burtenshaw/lora-without-regrets/blob/main/grpo.py) - Reinforcement learning with LoRA best practices ## Key findings in optimizing LoRA The authors recommend applying LoRA to all weight matrices rather than limiting it to attention layers, as increasing the rank does not compensate for this restriction. In TRL, this can be configured using `--lora_target_modules all-linear` to apply LoRA to all weight matrices. We were able to reproduce the results of the blog post using TRL and the SmolLM3 model. We trained the model for 500 steps on the [Math 220k dataset](https://huggingface.co/datasets/HuggingFaceH4/OpenR1-Math-220k-default-verified) with the reward function and configuration above. As you can see in the figure below, the LoRA model's average train reward curve matches the full fine-tuning curve. ![train reward](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/5.png) And most importantly, the LoRA model uses significantly less memory than the full fine-tuning model, as we can see in the figure below. ![memory usage](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/6.png) Here are the parameters we used to train the above models | Parameter | LoRA | Full FT | | --- | --- | --- | | `--model_name_or_path` | HuggingFaceTB/SmolLM3-3B | HuggingFaceTB/SmolLM3-3B | | `--dataset_name` | HuggingFaceH4/OpenR1-Math-220k-default-verified | HuggingFaceH4/OpenR1-Math-220k-default-verified | | `--learning_rate` | 1.0e-5 | 1.0e-6 | | `--max_completion_length` | 4096 | 4096 | | `--lora_r` | 1 | - | | `--lora_alpha` | 32 | - | | `--lora_dropout` | 0.0 | - | | `--lora_target_modules` | all-linear | - | Let's break down the key findings of the blog post and how we were able to reproduce them. ### 1. *LoRA performs better when applied to all weight matrices* The authors recommend applying LoRA to all weight matrices rather than limiting it to attention layers, as increasing the rank does not compensate for this restriction. ![all layers](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/1.png) Attention-only LoRA underperforms even when using a higher rank to match parameter count. In TRL, this can be configured using `--lora_target_modules all-linear` to apply LoRA to all weight matrices. In Python, we can do this like so: ```python from peft import LoraConfig peft_config = LoraConfig(target_modules="all-linear") ``` ### 2. *The adapter needs sufficient capacity to learn from the dataset* The blog post recommends using a sufficient LoRA rank to learn from the dataset. The rank determines the number of trainable parameters in the LoRA adapter. Therefore, "For datasets that exceed LoRA capacity, LoRA underperforms FullFT". ![learning rate](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/3.png) In the TRL script, we could use `--lora_r` to set the rank and adapt it based on the task and dataset we're training on. The blog post recommends the following ranks based on the task and dataset size: Reinforcement learning tasks typically require lower capacity, so smaller LoRA ranks can be used. This is because policy gradient algorithms extract roughly ~1 bit of information per episode, demanding minimal parameter capacity. The blog post defines the ideal dataset size for LoRA to match full fine-tuning as "Post-training scale". Which we can use to determine the recommended rank for SFT and RL LoRAs as: | Task Type | Dataset Size | Recommended Rank | | --- | --- | --- | | **SFT** | Post-training scale | 256 | | **RL** | Any size | 1-32 | ### 3. *"FullFT and high-rank LoRAs have similar learning curves"* Counterintuitively, the blog post recommends using a higher learning rate than for full fine-tuning. In the table above, we used 1.0e-5 for LoRA and 1.0e-6 for full fine-tuning. In the TRL script, we could use `--learning_rate` to set the learning rate. The \\( \frac{1}{r} \\) scaling in LoRA makes the optimal learning rate approximately rank-independent. ![learning rate](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/2.png) ### 4. *"In some scenarios, LoRA is less tolerant of large batch sizes than full fine-tuning."* The blog post recommends using an effective batch size < 32 because the authors found LoRA to be less tolerant of large batch sizes. This could not be mitigated by increasing the LoRA rank. In the TRL script, we could use `--per_device_train_batch_size` and `--gradient_accumulation_steps` to set the batch size. ![learning rate](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/lora_without_regret/4.png) ## Takeaways Using TRL, you can efficiently implement LoRA adapters to match full fine-tuning performance, applying the core insights (targeting all weight matrices, choosing the right rank, and managing batch size and learning rate) without the heavy compute cost of FullFT. ## Citation ```bibtex @article{schulman2025lora, title = {{LoRA Without Regret}}, author = {John Schulman and Thinking Machines Lab}, year = 2025, journal = {Thinking Machines Lab: Connectionism}, doi = {10.64434/tml.20250929}, note = {https://thinkingmachines.ai/blog/lora/} } ``` ================================================ FILE: docs/source/merge_model_callback.md ================================================ # MergeModelCallback [[autodoc]] experimental.merge_model_callback.MergeModelCallback ================================================ FILE: docs/source/minillm_trainer.md ================================================ # MiniLLM Trainer [![All_models-MiniLLM-blue](https://img.shields.io/badge/All_models-MiniLLM-blue)](https://huggingface.co/models?other=minillm,trl) ## Overview TRL supports the MiniLLM Trainer for distilling large language models into smaller ones using reverse KLD for better precision, quality, and performance, as described in the paper [Knowledge Distillation of Large Language Models](https://huggingface.co/papers/2306.08543) by [Yuxian Gu](https://huggingface.co/t1101675), [Li Dong](https://huggingface.co/unilm), [Furu Wei](https://huggingface.co/thegenerality), and Minlie Huang. The abstract from the paper is the following: > Knowledge Distillation (KD) is a promising technique for reducing the high computational demand of large language models (LLMs). However, previous KD methods are primarily applied to white-box classification models or training small models to imitate black-box model APIs like ChatGPT. How to effectively distill the knowledge from white-box generative LLMs is still under-explored, which becomes more and more important with the prosperity of LLMs. In this work, we propose MiniLLM that distills smaller language models from generative larger language models. We first replace the forward Kullback-Leibler divergence (KLD) objective in the standard KD approaches with reverse KLD, which is more suitable for KD on generative language models, to prevent the student model from overestimating the low-probability regions of the teacher distribution. Then, we derive an effective optimization approach to learn this objective. Extensive experiments in the instruction-following setting show that the MiniLLM models generate more precise responses with the higher overall quality, lower exposure bias, better calibration, and higher long-text generation performance. Our method is also scalable for different model families with 120M to 13B parameters. We will release our code and model checkpoints at https://aka.ms/MiniLLM. This post-training method was contributed by [Yuxian Gu](https://huggingface.co/t1101675). It is a generalized version of [Think Machine Lab's On-Policy Distillation](https://thinkingmachines.ai/blog/on-policy-distillation/), with the option to add distribution-level single-step distillation signals (like GKD when `beta=1`) and long-context reverse KLD signals. $$ \begin{align} L_{\text{MiniLLM}}&=\alpha_1\mathbb{E}_{x\sim \pi_{\theta}}\sum_{t'=t}^{|x|}\frac{\gamma^{t'-t}}{\sum_{t'}\gamma^{t'-t}}\left[\log \frac{\pi_{\theta}(x_{t'+1}|x_{1..t'})}{\pi_{\text{teacher}}(x_{t'+1}|x_{1..t'})}\right] \\ &+ \alpha_2\mathbb{E}_{x\sim \pi_{\theta}} \text{KL}\left[\pi_\theta(\cdot|x_{1..t})||\pi_{\text{teacher}}(\cdot | x_{1..t})\right]. \end{align} $$ When \\( \alpha_1=1 \\), \\( \alpha_2=0 \\), \\( \gamma=0 \\), which corresponds to ```python from trl.experimental.minillm import MiniLLMConfig training_args = MiniLLMConfig( rkl_advantage=True, single_step_decomposition=False, gamma=False ) ``` \\( L_{\text{MiniLLM}} \\) becomes the on-policy KD implemented in [Tinker](https://github.com/thinking-machines-lab/tinker-cookbook/blob/5d08be6d130596b7bedd02197861c41fa81ea436/tinker_cookbook/distillation/train_on_policy.py#L88): $$ L_{\text{tinker}}=\mathbb{E}_{x\sim \pi_{\theta}}\left[\log \frac{\pi_{\theta}(x_{t'+1}|x_{1..t'})}{\pi_{\text{teacher}}(x_{t'+1}|x_{1..t'})}\right]. $$ When \\( \alpha_1=0 \\), \\( \alpha_2=1 \\), which corresponds to ```python from trl.experimental.minillm import MiniLLMConfig training_args = MiniLLMConfig( rkl_advantage=False, single_step_decomposition=True ) ``` \\( L_{\text{MiniLLM}} \\) becomes the reverse KLD version of the GKD loss as in [GKD Trainer](./gkd.md): $$ L_{\text{GKD-RKL}}=\mathbb{E}_{x\sim \pi_{\theta}} \text{KL}\left[\pi_\theta(\cdot|x_{1..t})||\pi_{\text{teacher}}(\cdot | x_{1..t})\right]. $$ ## MiniLLMTrainer [[autodoc]] experimental.minillm.MiniLLMTrainer - train - save_model - push_to_hub ## MiniLLMConfig [[autodoc]] experimental.minillm.MiniLLMConfig ================================================ FILE: docs/source/nash_md_trainer.md ================================================ # Nash-MD Trainer [![model badge](https://img.shields.io/badge/All_models-Nash--MD-blue)](https://huggingface.co/models?other=nash-md,trl) ## Overview Nash-MD was proposed in the paper [Nash Learning from Human Feedback](https://huggingface.co/papers/2312.00886) by Rémi Munos, [Michal Valko](https://huggingface.co/misovalko), Daniele Calandriello, Mohammad Gheshlaghi Azar, Mark Rowland, Daniel Guo, Yunhao Tang, Matthieu Geist, Thomas Mésnard, and Andrea Michi. The abstract from the paper is the following: > Reinforcement learning from human feedback (RLHF) has emerged as the main paradigm for aligning large language models (LLMs) with human preferences. Typically, RLHF involves the initial step of learning a reward model from human feedback, often expressed as preferences between pairs of text generations produced by a pre-trained LLM. Subsequently, the LLM's policy is fine-tuned by optimizing it to maximize the reward model through a reinforcement learning algorithm. However, an inherent limitation of current reward models is their inability to fully represent the richness of human preferences and their dependency on the sampling distribution. In this study, we introduce an alternative pipeline for the fine-tuning of LLMs using pairwise human feedback. Our approach entails the initial learning of a preference model, which is conditioned on two inputs given a prompt, followed by the pursuit of a policy that consistently generates responses preferred over those generated by any competing policy, thus defining the Nash equilibrium of this preference model. We term this approach Nash learning from human feedback (NLHF). In the context of a tabular policy representation, we present a novel algorithmic solution, Nash-MD, founded on the principles of mirror descent. This algorithm produces a sequence of policies, with the last iteration converging to the regularized Nash equilibrium. Additionally, we explore parametric representations of policies and introduce gradient descent algorithms for deep-learning architectures. To demonstrate the effectiveness of our approach, we present experimental results involving the fine-tuning of a LLM for a text summarization task. We believe NLHF offers a compelling avenue for preference learning and policy optimization with the potential of advancing the field of aligning LLMs with human preferences. This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif) and [Daniil Tiapkin](https://huggingface.co/dtiapkin), [Pierre Ménard](https://huggingface.co/menardprr), Daniele Calandriello and [Quentin Gallouédec](https://huggingface.co/qgallouedec). ## Quick start This example demonstrates how to train a model using the Nash-MD method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model and [`experimental.judges.PairRMJudge`] as a judge. We use the prompts from the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). You can view the prompts in the dataset here: Below is the script to train the model: ```python # train_nash_md.py from datasets import load_dataset from trl.experimental.judges import PairRMJudge from trl.experimental.nash_md import NashMDConfig, NashMDTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") judge = PairRMJudge() train_dataset = load_dataset("trl-lib/ultrafeedback-prompt", split="train") training_args = NashMDConfig(output_dir="Qwen2-0.5B-NashMD") trainer = NashMDTrainer( model=model, judge=judge, args=training_args, processing_class=tokenizer, train_dataset=train_dataset ) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_nash_md.py ``` Distributed across 8 GPUs, the training takes approximately 3 hours. To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-NashMD) performs, you can use the [Transformers Chat CLI](https://huggingface.co/docs/transformers/quicktour#chat-with-text-generation-models).
$ transformers chat trl-lib/Qwen2-0.5B-NashMD
<quentin_gallouedec>:
What is the best programming language?

<trl-lib/Qwen2-0.5B-NashMD>:
The best programming language depends on personal preference, the complexity of the project, and the specific requirements of the task. Some programming languages that are often recommended include Python, Java, and JavaScript, and there are many other languages to choose from depending on individual needs.
## Expected dataset type Nash-MD requires a [prompt-only dataset](dataset_formats#prompt-only). The [`experimental.nash_md.NashMDTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ## Usage tips ### Use a reward model Instead of a judge, you can chose to use a reward model -- see [Reward Bench](https://huggingface.co/spaces/allenai/reward-bench) for a leaderboard of public models you can use. Below is a code example showing how to replace a judge with the [trl-lib/Qwen2-0.5B-Reward](https://huggingface.co/trl-lib/Qwen2-0.5B-Reward) model: ```diff - from trl.experimental.judges import PairRMJudge + from transformers import AutoModelForSequenceClassification - judge = PairRMJudge() + reward_model = AutoModelForSequenceClassification.from_pretrained("trl-lib/Qwen2-0.5B-Reward", num_labels=1) trainer = NashMDTrainer( ... - judge=judge, + reward_funcs=reward_model, ) ``` > [!WARNING] > Make sure that the SFT model and reward model use the _same_ chat template and the same tokenizer. Otherwise, you may find the model completions are scored incorrectly during training. ### Encourage EOS token generation We may want the model to generate completions within a given length. During training, the model will generate completions up to the maximum length specified in the `max_new_tokens` argument of [`experimental.nash_md.NashMDConfig`]. If you want to penalize the model for not generating an EOS token before reaching the maximum length, you can use the `missing_eos_penalty` argument of [`experimental.nash_md.NashMDConfig`]: ```python training_args = NashMDConfig(..., max_new_tokens=128, missing_eos_penalty=1.0) ``` ### Logging Completions To better understand your model’s behavior during training, you can log sample completions periodically using the [`LogCompletionsCallback`]. ```python trainer = NashMDTrainer(..., eval_dataset=eval_dataset) completions_callback = LogCompletionsCallback(trainer, num_prompts=8) trainer.add_callback(completions_callback) ``` This callback logs the model's generated completions directly to Weights & Biases. ![Logged Completions](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/wandb_completions.png) ## Example script We provide an example script to train a model using the Nash-MD method. The script is available in [`examples/scripts/nash_md.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/nash_md.py) To test the online DPO script with the [Qwen2.5 0.5B model](https://huggingface.co/trl-lib/Qwen/Qwen2.5-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback), run the following command: ```bash python examples/scripts/nash_md.py \ --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --judge pair_rm \ --dataset_name trl-lib/ultrafeedback-prompt \ --learning_rate 5.0e-7 \ --output_dir Qwen2.5-0.5B-NashMD-PairRM \ --warmup_steps 0.1 \ --push_to_hub ``` ## Logged metrics While training and evaluating, we record the following reward metrics: * `loss/kl`: The mean KL divergence between the model and reference data. * `objective/entropy`: The mean entropy of the model and reference data. * `loss/score`: The mean reinforce score loss. * `rewards/chosen`: The mean scores (according to the reward model) of the model completions. * `rewards/rejected`: The mean scores (according to the reward model) of the mixture completions. * `rewards/probabilities`: The mean probability (according to the reward model or judge) of the model completions chosen vs the mixture completion. * `rewards/accuracies`: The accuracies of the Nash-MD's implicit reward model. * `rewards/margins`: The mean reward margin (according to reward model) between the chosen and mixture completions. * `logps/chosen`: The mean log probabilities of the chosen completions. * `logps/rejected`: The mean log probabilities of the reference completions. * `val/model_contain_eos_token`: The amount of times the model's output contains the eos token. * `val/ref_contain_eos_token`: The amount of times the mixture's output contains the eos token. * `beta`: The parameter that controls the weight of the loss term representing the deviation from the reference model. Typically fixed, but can be made dynamic by passing a list to [`experimental.nash_md.NashMDConfig`]. * `mixture_coef`: Logit mixture coefficient for the model and reference model. Typically fixed, but can be made dynamic by passing a list to [`experimental.nash_md.NashMDConfig`]. ## NashMDTrainer [[autodoc]] experimental.nash_md.NashMDTrainer - train - save_model - push_to_hub ## NashMDConfig [[autodoc]] experimental.nash_md.NashMDConfig ================================================ FILE: docs/source/nemo_gym.md ================================================ # NeMo Gym Integration NVIDIA NeMo Gym is a library for building RL environments for large language models. This integration enables training models in NeMo Gym environments using TRL's GRPOTrainer with vLLM server mode. The integration supports multi-step and multi-turn rollouts, multi-environment training, and any NeMo Gym environment (thoroughly tested: workplace assistant, reasoning gym, MCQA, and math with judge). ## Why NeMo Gym - **Production-Ready Scale**: Tested for frontier model training with diverse environments running in parallel across math, coding, tool use, reasoning, and more. - **Multi-Verifier Training**: Supports algorithmic verification, LLM-as-a-judge, and custom verification logic in a single training run. - **Decoupled Architecture**: Build agents and environments independently from the training loop—no RL framework expertise required. - **OpenAI-Compatible API**: All environments use the standardized OpenAI Responses API for seamless integration with vLLM, OpenAI models, and other endpoints. ## Available Environments NeMo Gym provides training-ready environments across multiple domains, including but not limited to: | Environment | Domain | Description | |-------------|--------|-------------| | Workplace Assistant | Agent | Multi-step tool calling in common office scenarios (calendar, email, and more) | | Math with Judge | Math | Math problems with algorithmic or judge-based verification | | Code Gen | Coding | Competitive programming problems with code execution | | MCQA | Knowledge | Multiple-choice question answering | | Instruction Following | Instruction Following | IFEval/IFBench style tasks | | Reasoning Gym | Multiple | Single-step procedurally generated verifiable tasks across domains | For a complete list of available training environments, refer to the [NeMo Gym repository](https://github.com/NVIDIA-NeMo/Gym#-available-resource-servers). ## Before You Start Complete these one-time setup steps before running training. ### Install TRL and NeMo Gym 1. **Install TRL with vLLM extras** ```bash cd trl/ uv venv source .venv/bin/activate uv sync --extra vllm ``` 1. **Install NeMo Gym** ```bash # deactivate trl venv deactivate git clone https://github.com/NVIDIA-NeMo/Gym.git cd Gym uv venv --python 3.12 source .venv/bin/activate uv sync ``` ### Prepare a Dataset Many NeMo Gym datasets used to train Nemotron models are available on Hugging Face. Use `ng_prepare_data` to download and prepare datasets. This command: - Downloads the dataset from Hugging Face - Validates the data format - Adds an `agent_ref` field to each example that tells NeMo Gym which agent server should handle that example > **Note**: `train_multi_environment.py` adds the `agent_ref` field when loading datasets, so this step is optional if datasets are created another way. 1. **Set Hugging Face Token** Create `env.yaml` in `Gym/` with your HF token: ```yaml hf_token: ``` 1. **Prepare Dataset** ```bash # Enter Gym and activate the venv cd Gym source .venv/bin/activate # Set config paths config_paths="responses_api_models/vllm_model/configs/vllm_model.yaml,\ resources_servers/workplace_assistant/configs/workplace_assistant.yaml" # Download data and prep for training ng_prepare_data "+config_paths=[${config_paths}]" \ +output_dirpath=data/workplace_assistant \ +mode=train_preparation \ +should_download=true \ +data_source=huggingface ``` This creates `train.jsonl` and `validation.jsonl` files in `data/workplace_assistant/`. To create a new environment, refer to the [environment creation guide](https://docs.nvidia.com/nemo/gym/latest/contribute/environments/new-environment.html). We suggest running an existing one first! #### Dataset Format NeMo Gym datasets are stored as JSONL. Each line contains a task with input messages, tool definitions, metadata such as ground truth for verification, and an agent server reference. The following example shows the workplace dataset structure. Metadata fields can differ between datasets, as long as the corresponding resources server uses the fields appropriately. ```json { "responses_create_params": { "input": [ {"role": "system", "content": "..."}, {"role": "user", "content": "Move any of jinsoo's tasks that are in review to completed"} ], "tools": [...], "parallel_tool_calls": false, "temperature": 1 }, "ground_truth": [ {"name": "project_management_update_task", "arguments": "{...}"}, ... ], "category": "workbench_project_management", "environment_name": "workbench", "agent_ref": { "type": "responses_api_agents", "name": "workplace_assistant_simple_agent" } } ``` ## Interactive Training For development and testing on a single node. ### Set Up 1. **Update Environment Config** Update `env.yaml` in `Gym/` to include model information: ```yaml policy_base_url: http://127.0.0.1:8000/v1 policy_api_key: EMPTY policy_model_name: Qwen/Qwen2.5-1.5B-Instruct hf_token: ... ``` 2. **Update Training Config** Update `examples/scripts/nemo_gym/config.yaml` to point to the dataset generated above, and any other optional modifications. ### Run Training The following steps run in 3 terminals. It can also be ran with processes in the background, or using tmux. 1. **Start NeMo Gym Servers** (Terminal 1) ```bash cd Gym/ source .venv/bin/activate config_paths="resources_servers/workplace_assistant/configs/workplace_assistant.yaml,\ responses_api_models/vllm_model/configs/vllm_model_for_training.yaml" ng_run "+config_paths=[${config_paths}]" ``` This starts: - **Agent server**: Orchestrates rollouts using resource servers and model servers - **Resources server**: Supports environment logic such as state-management, tool implementations, and task verification - **Model server**: Adapts vLLM server requests to support NeMo Gym agents and on-policy RL training while ensuring OpenAI API compatibility - **Head server**: Manages servers used in training enabling their discovery 1. **Start TRL vLLM Server on GPU 0** (Terminal 2) ```bash cd trl/ source .venv/bin/activate CUDA_VISIBLE_DEVICES=0 trl vllm-serve \ --model Qwen/Qwen2.5-1.5B-Instruct \ --max-model-len 16384 \ --host 0.0.0.0 \ --port 8000 ``` 1. **Run Training on GPU 1** (Terminal 3) ```bash source trl/.venv/bin/activate cd trl/examples/scripts/nemo_gym export WANDB_API_KEY=... uv add omegaconf CUDA_VISIBLE_DEVICES=1 python train_multi_environment.py --config config.yaml ``` ## Multi-Node Training with Slurm An example five-node training script is provided in `submit.sh`. Nodes one through four run the training algorithm, while node five runs vLLM inference for NeMo Gym agent rollouts. 1. **Configure the Script** Update `submit.sh` with your Slurm account, partition, paths to your project directory, and updated training configs. 1. **Submit the Job** ```bash sbatch submit.sh ``` 1. **Monitor Training** ```bash tail -f logs//* ``` > **Tip**: Set up wandb logging for detailed training metrics. For more details on TRL's vLLM integration, refer to the vLLM integration page. ## Multi-Environment Training Train on multiple NeMo Gym environments simultaneously. This allows learning diverse capabilities, such as tool calling and math reasoning, in a single training run. 1. **Prepare Individual Datasets** Prepare datasets for each environment. The workplace assistant dataset was prepared above. Now lets create a dataset for the mini sudoku environment implemented by the reasoning gym resources server in NeMo Gym: ```bash cd Gym source .venv/bin/activate uv add reasoning-gym cd resources_servers/reasoning_gym python scripts/create_dataset.py \ --task mini_sudoku \ --size 2000 \ --seed 42 \ --output data/reasoning_gym/train_mini_sudoku.jsonl python scripts/create_dataset.py \ --task mini_sudoku \ --size 50 \ --seed 24 \ --output data/reasoning_gym/val_mini_sudoku.jsonl ``` 1. **Create Combined Dataset** Combine datasets into a single file with tasks from both environments: ```bash cat data/workplace_assistant/train_workplace.jsonl data/reasoning_gym/train_mini_sudoku.jsonl | shuf > train_multi_env.jsonl ``` > **Tip**: Ensure datasets are the same size before shuffling for an even blend of tasks. Repeat for the validation dataset. 1. **Update Training Config** Update the config to point to the combined dataset: ```yaml model_name: "Qwen/Qwen3-4B-Instruct-2507" dataset_path: "/path/to/data/train_multi_env.jsonl" eval_dataset_path: "/path/to/data/val_multi_env.jsonl" task: "workplace-sudoku" # used in wandb run name output_dir: "outputs/nemo_gym_multi_env" # ... rest of config same ``` 1. **Update ng_run** Whether training interactively or via Slurm, update the `ng_run` command to include config files from each resources server: ```bash cd Gym source .venv/bin/activate config_paths="responses_api_models/vllm_model/configs/vllm_model.yaml,\ resources_servers/workplace_assistant/configs/workplace_assistant.yaml,\ resources_servers/reasoning_gym/configs/reasoning_gym.yaml" ng_run "+config_paths=[${config_paths}]" ``` This starts servers for both environments. The training script automatically routes each example to the correct agent server based on its `agent_ref` field. 1. **Run Training** Update the Slurm submission script to use the new training config and both `ng_run` resources server configs, then submit the job as before. The training script reads `agent_ref` from each example's metadata, routes requests to the correct NeMo Gym agent server, and handles different agents and environments in the same batch. ## Resources - [NeMo Gym GitHub](https://github.com/NVIDIA-NeMo/Gym) - [NeMo Gym Documentation](https://docs.nvidia.com/nemo/gym/latest/) - [Training Script](https://github.com/huggingface/trl/blob/main/examples/scripts/nemo_gym/train_multi_environment.py) - [TRL GRPO Trainer](grpo_trainer) ================================================ FILE: docs/source/online_dpo_trainer.md ================================================ # Online DPO Trainer [![model badge](https://img.shields.io/badge/All_models-Online_DPO-blue)](https://huggingface.co/models?other=online-dpo,trl) ## Overview Online DPO was proposed in [Direct Language Model Alignment from Online AI Feedback](https://huggingface.co/papers/2402.04792) by Shangmin Guo, Biao Zhang, Tianlin Liu, Tianqi Liu, Misha Khalman, Felipe Llinares, Alexandre Rame, Thomas Mesnard, Yao Zhao, Bilal Piot, Johan Ferret, and Mathieu Blondel. The abstract from the paper is the following: > Direct alignment from preferences (DAP) methods, such as DPO, have recently emerged as efficient alternatives to reinforcement learning from human feedback (RLHF), that do not require a separate reward model. However, the preference datasets used in DAP methods are usually collected ahead of training and never updated, thus the feedback is purely offline. Moreover, responses in these datasets are often sampled from a language model distinct from the one being aligned, and since the model evolves over training, the alignment phase is inevitably off-policy. In this study, we posit that online feedback is key and improves DAP methods. Our method, online AI feedback (OAIF), uses an LLM as annotator: on each training iteration, we sample two responses from the current model and prompt the LLM annotator to choose which one is preferred, thus providing online feedback. Despite its simplicity, we demonstrate via human evaluation in several tasks that OAIF outperforms both offline DAP and RLHF methods. We further show that the feedback leveraged in OAIF is easily controllable, via instruction prompts to the LLM annotator. This post-training method was contributed by [Michael Noukhovitch](https://huggingface.co/mnoukhov), [Shengyi Costa Huang](https://huggingface.co/vwxyzjn), [Quentin Gallouédec](https://huggingface.co/qgallouedec), and [Edward Beeching](https://huggingface.co/edbeeching). ## Quick start This example demonstrates how to train a model using the online DPO method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model and [`experimental.judges.PairRMJudge`] as a judge. We use the prompts from the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). You can view the prompts in the dataset here: Below is the script to train the model: ```python # train_online_dpo.py from datasets import load_dataset from trl.experimental.judges import PairRMJudge from trl.experimental.online_dpo import OnlineDPOConfig, OnlineDPOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") judge = PairRMJudge() train_dataset = load_dataset("trl-lib/ultrafeedback-prompt", split="train") training_args = OnlineDPOConfig(output_dir="Qwen2-0.5B-OnlineDPO") trainer = OnlineDPOTrainer( model=model, judge=judge, args=training_args, processing_class=tokenizer, train_dataset=train_dataset ) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_online_dpo.py ``` Distributed across 8 GPUs, the training takes approximately 1 hour. You can verify the training progress by checking the reward graph. An increasing trend in both the reward for rejected and chosen completions indicates that the model is improving and generating better responses over time. ![](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/online-dpo-qwen2.png) To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-OnlineDPO) performs, you can use the [Transformers Chat CLI](https://huggingface.co/docs/transformers/quicktour#chat-with-text-generation-models).
$ transformers chat trl-lib/Qwen2-0.5B-OnlineDPO
<quentin_gallouedec>:
What is the best programming language?

<trl-lib/Qwen2-0.5B-OnlineDPO>:
The best programming language depends on your specific needs and priorities. Some people prefer imperative programming languages (like Haskell or Lisp), while others prefer functional programming languages (like Scala or Python). It's important to consider your work style, programming environment, and project requirements when choosing a programming language.
## Expected dataset type Online DPO only requires a [prompt-only dataset](dataset_formats#prompt-only) (unlike offline DPO, that expects [preference dataset](dataset_formats#preference)). The [`experimental.online_dpo.OnlineDPOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ## Usage tips ### Use a reward model Instead of a judge, you can chose to use a reward model -- see [Reward Bench](https://huggingface.co/spaces/allenai/reward-bench) for a leaderboard of public models you can use. Below is a code example showing how to replace a judge with the [trl-lib/Qwen2-0.5B-Reward](https://huggingface.co/trl-lib/Qwen2-0.5B-Reward) model: ```diff - from trl.experimental.judges import PairRMJudge + from transformers import AutoModelForSequenceClassification - judge = PairRMJudge() + reward_model = AutoModelForSequenceClassification.from_pretrained("trl-lib/Qwen2-0.5B-Reward", num_labels=1) + reward_tokenizer = AutoTokenizer.from_pretrained("trl-lib/Qwen2-0.5B-Reward") trainer = OnlineDPOTrainer( ... - judge=judge, + reward_funcs=reward_model, + reward_processing_class=reward_tokenizer, ... ) ``` ### Encourage EOS token generation When using a reward model, we may want the model to generate completions within a given length. During training, the model will generate completions up to the maximum length specified in the `max_new_tokens` argument of [`experimental.online_dpo.OnlineDPOConfig`]. If you want to penalize the model for not generating an EOS token before reaching the maximum length, you can use the `missing_eos_penalty` argument of [`experimental.online_dpo.OnlineDPOConfig`]: ```python training_args = OnlineDPOConfig(..., max_new_tokens=128, missing_eos_penalty=1.0) ``` ### Logging Completions To better understand your model’s behavior during training, you can log sample completions periodically using the [`LogCompletionsCallback`]. ```python trainer = OnlineDPOTrainer(..., eval_dataset=eval_dataset) completions_callback = LogCompletionsCallback(trainer, num_prompts=8) trainer.add_callback(completions_callback) ``` This callback logs the model's generated completions directly to Weights & Biases. ![Logged Completions](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/wandb_completions.png) ## Example script We provide an example script to train a model using the online DPO method. The script is available in [`examples/scripts/dpo_online.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/dpo_online.py) To test the online DPO script with the [Qwen2.5 0.5B model](https://huggingface.co/trl-lib/Qwen/Qwen2.5-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback), run the following command: ```bash python examples/scripts/dpo_online.py \ --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --judge pair_rm \ --dataset_name trl-lib/ultrafeedback-prompt \ --learning_rate 5.0e-7 \ --output_dir Qwen2.5-0.5B-Online-DPO-PairRM \ --warmup_steps 0.1 \ --push_to_hub ``` ## Logged metrics While training and evaluating, we record the following reward metrics. Here is an example [tracked run at Weights and Biases](https://wandb.ai/huggingface/trl/runs/w4apmsi9) * `objective/kl`: The mean Kullback-Leibler (KL) divergence between the current model and reference model. * `objective/entropy`: The mean entropy of the model, indicating the randomness of the actions chosen by the model. * `objective/non_score_reward`: The mean reward from non-score-related sources, basically `beta * kl.sum(1)`, where `beta` is the KL penalty coefficient and `kl` is the per-token KL divergence. * `objective/rlhf_reward`: The mean RLHF reward, which is `scores - non_score_reward`. The `rlhf_reward` is the ultimate objective of online DPO training. If training works as intended, this metric should keep going up. * `objective/scores`: The mean scores returned by the reward model. * `objective/scores_margin`: The mean score margin (according to the external reward model) between the chosen and rejected completions. * `rewards/chosen`: The mean reward (according to online DPO's implicit reward model)of the chosen completions. * `rewards/rejected`: The mean reward (according to online DPO's implicit reward model) of the rejected completions. * `rewards/accuracies`: The accuracies of the online DPO's implicit reward model. * `rewards/margins`: The mean reward margin (according to online DPO's implicit reward model) between the chosen and rejected completions. * `logps/chosen`: The mean log probabilities of the chosen completions. * `logps/rejected`: The mean log probabilities of the rejected completions. * `val/contain_eos_token`: The fraction of completions which contain an EOS token. * `beta`: The parameter that controls the weight of the loss term representing the deviation from the reference model. Typically fixed, but can be made dynamic by passing a list to [`experimental.online_dpo.OnlineDPOConfig`]. ## Benchmark experiments To validate the online DPO implementation works, we ran experiments with the Pythia 1B, 2.8B, and 6.9B models on a single node of 8 x H100s. Here are the commands we used to run the experiments. We take the SFT / RM models directly from [The N+ Implementation Details of RLHF with PPO: A Case Study on TL;DR Summarization](https://huggingface.co/papers/2403.17031). ```shell # 1B Online DPO experiment accelerate launch --config_file examples/accelerate_configs/multi_gpu.yaml \ examples/scripts/dpo_online.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-1b-deduped-tldr-online-dpo \ --beta 0.1 \ --per_device_train_batch_size 8 \ --gradient_accumulation_steps 2 \ --num_train_epochs 3 \ --max_new_tokens 53 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --save_steps 0.1 \ --push_to_hub # 2.8B Online DPO experiment accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/dpo_online.py \ --model_name_or_path trl-lib/pythia-2.8b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-2.8b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-2.8b-deduped-tldr-online-dpo \ --beta 0.1 \ --per_device_train_batch_size 8 \ --gradient_accumulation_steps 2 \ --num_train_epochs 3 \ --max_new_tokens 53 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --save_steps 0.1 \ --push_to_hub # 6.9B Online DPO experiment accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/dpo_online.py \ --model_name_or_path trl-lib/pythia-6.9b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-6.9b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-6.9b-deduped-tldr-online-dpo \ --beta 0.1 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --num_train_epochs 3 \ --max_new_tokens 53 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --save_steps 0.1 \ --push_to_hub ``` Checkpoints and experiment tracking are available at: * [🤗 Model checkpoints](https://huggingface.co/collections/trl-lib/online-dpo-66acd3fa38a331a9cd457b07) * [🐝 Tracked experiment](https://wandb.ai/huggingface/trl/reports/Online-DPO-experiments-for-TL-DR-summarisation--Vmlldzo5MTczMDU0) To evaluate, we use [vLLM](https://github.com/vllm-project/vllm) to load the checkpoints and GPT-4o mini as a judge model to evaluate the generated TL;DR against the reference TL;DR. For more information on how to use judges, see [Judges](judges). ```bash $ python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 33.00% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/pythia-6.9b-deduped-tldr-sft --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 41.50% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/pythia-1b-deduped-tldr-online-dpo --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 62.60% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/pythia-6.9b-deduped-tldr-online-dpo --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 74.20% ``` We can then plot the RLHF scaling chart. ```python import matplotlib.pyplot as plt results = { "SFT": {1.0e9: 0.21, 2.8e9: 0.27, 6.9e9: 0.316}, "online-dpo": {1.0e9: 0.542, 2.8e9: 0.746, 6.9e9: 0.796}, "offline-dpo": {1.0e9: 0.422, 2.8e9: 0.517, 6.9e9: 0.701}, } plt.plot(results["SFT"].keys(), results["SFT"].values(), label="SFT", marker="o") plt.plot(results["online-dpo"].keys(), results["online-dpo"].values(), label="Online-dpo with RM judge", marker="o") plt.plot(results["offline-dpo"].keys(), results["offline-dpo"].values(), label="Offline-dpo", marker="o") plt.axhline(y=0.5, color="black", linestyle="-.", label="Human reference summary") plt.xscale("log") plt.xlabel("Model size") plt.ylabel("Win rate against reference summaries\n(according to GPT-4-0613)") plt.title("DPO scaling by model size") plt.legend() plt.xlim(5e8, 1.2e10) plt.xticks([1e9, 3e9, 1e10], ["1B", "3B", "10B"]) plt.grid(True, which="both", ls="--", c="0.7") plt.tight_layout() plt.show() ``` The online DPO checkpoint gets increasingly more win rate as we scale up the model sizes. This is a good sign that the online DPO implementation is working as intended. ## OnlineDPOTrainer [[autodoc]] experimental.online_dpo.OnlineDPOTrainer - train - save_model - push_to_hub ## OnlineDPOConfig [[autodoc]] experimental.online_dpo.OnlineDPOConfig ================================================ FILE: docs/source/openenv.md ================================================ # OpenEnv Integration for Training LLMs with Environments [OpenEnv](https://github.com/meta-pytorch/OpenEnv) is an open-source framework from Meta's PyTorch team for defining, deploying, and interacting with environments in reinforcement learning (RL) and agentic workflows. It offers [Gymnasium-style APIs](https://gymnasium.farama.org) (e.g., `reset()` and `step()`) to interface with environments in a standard manner, and supports running these environments as backend servers (for example, via HTTP or containerised execution). You can find a collection of ready-to-use OpenEnv environments on the [Hugging Face Hub](https://huggingface.co/collections/openenv/openenv-environment-hub). In this guide, we’ll focus on **how to integrate OpenEnv with TRL**, but feel free to explore the links above to dive deeper into OpenEnv itself. > [!NOTE] > You can explore ready-to-use example [scripts](example_overview#scripts) and [notebooks](example_overview#notebooks) in the Examples Overview. > [!NOTE] > Explore the [OpenEnv docs](https://meta-pytorch.org/OpenEnv/) for more details. ## Installation To use OpenEnv with TRL, install the environment package. You have two options: **Option A - Install from HF Space (recommended):** ```bash pip install git+https://huggingface.co/spaces/openenv/echo_env ``` > [!TIP] > You can also install the core package from PyPI with `pip install "openenv-core[core]>=0.2.1"`, but note that environment-specific dependencies may need to be installed separately. **Option B - Clone OpenEnv repo (for development):** ```bash git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/echo_env pip install -e . ``` ## Using `rollout_func` with OpenEnv environments TRL's [`GRPOTrainer`] supports _custom rollout logic_ through the `rollout_func` argument. This lets you override the trainer's default text-generation loop and directly interact with OpenEnv environments — for instance, to compute environment-driven rewards instead of relying solely on model-based signals. ### Rollout Function Signature A rollout function must have the following signature: ```python def rollout_func( prompts: list[str], trainer: GRPOTrainer, ) -> dict[str, list]: """ Custom rollout function for generation and reward computation. Args: prompts: List of prompts routed to the current process trainer: Active GRPOTrainer (gives access to tokenizer, config and helper utilities) Returns: Dictionary containing: - prompt_ids: List of token IDs for each prompt - completion_ids: List of token IDs for each completion - logprobs: List of log probabilities for each token - Any additional fields are forwarded to reward functions as kwargs """ pass ``` > [!NOTE] > Any extra fields in the returned dictionary (beyond the required three) are automatically forwarded to your reward functions. This makes it easy to propagate signals such as environment rewards or auxiliary metrics from the rollout step. ### Integration pattern The typical pattern when combining OpenEnv with TRL looks like this: 1. Start or connect to an OpenEnv environment (e.g., a Dockerized env or HTTP endpoint). 2. Generate completions from your model — either via `trl.experimental.openenv.generate_rollout_completions` when using colocated vLLM, or by hitting your inference server when using vLLM in server mode. 3. Step through the environment using each completion to compute rewards or metrics. 4. Add environment results (e.g., `env_reward`) to the rollout result dict. 5. Access those rewards inside your reward function via `**kwargs`. By using OpenEnv in this loop, you can: * Train with realistic or interactive feedback (not just static reward functions). * Plug in custom simulators, web APIs, or evaluators as environments. * Pass structured reward signals back into RL training seamlessly. ### vLLM Modes TRL supports two vLLM execution modes for generation: - **`colocate` mode** (default): vLLM runs in the same process as training. Requires 1 GPU. Use `trl.experimental.openenv.generate_rollout_completions` for generation. - **`server` mode**: vLLM runs as a separate server process. Requires at least 2 GPUs (one for vLLM server, one for training), but is highly scalable: - You can allocate multiple GPUs to the vLLM server for tensor parallelism (faster inference) - You can run multiple training processes that share the same vLLM server - You can use different GPU types for inference vs training (e.g., A100 for vLLM, H100 for training) - The vLLM server can serve multiple experiments simultaneously - Use `trl.experimental.openenv.generate_rollout_completions` which will communicate with the server via `vllm_server_url` Configure the mode via `GRPOConfig`: ```python # Colocate mode (1 GPU) args = GRPOConfig( use_vllm=True, vllm_mode="colocate", # ... other args ) # Server mode (2+ GPUs, scalable) args = GRPOConfig( use_vllm=True, vllm_mode="server", vllm_server_base_url="http://localhost:8000", # ... other args ) # Example: Start vLLM server with multiple GPUs for tensor parallelism # CUDA_VISIBLE_DEVICES=0,1,2,3 trl vllm-serve --model Qwen/Qwen3-1.7B --tensor-parallel-size 4 ``` ## Running the Environments You can run OpenEnv environments in three different ways: - We can load the environment from the Hugging Face Hub and execute it as a Docker container. - We can connect to a hosted environment running on the Hugging Face Hub. - We can launch the environment directly using Uvicorn in Python. **Load from Hugging Face Hub** *(recommended)* We can use the [`from_hub`](https://meta-pytorch.org/OpenEnv/core/#core.http_env_client.HTTPEnvClient.from_hub) method to load the environment from the hub. This method will automatically start a Docker container for the environment on your local machine. [`openenv/echo-env`](https://huggingface.co/spaces/openenv/echo_env) is the repo_id of the space on the hub. ```python env = EchoEnv.from_hub("openenv/echo-env") ``` If you want to launch the environment manually, you can use the following command to pull and run the Docker container: ```bash docker run -d -p 8001:8000 --platform linux/amd64 registry.hf.space/openenv-echo-env:latest ``` And then you can connect to the environment using the following code: ```python env = EchoEnv(base_url="http://0.0.0.0:8001") ``` Here, we map the ports from 8001 to 8000 to make space for a vLLM server, but you will need to manage the ports for your local machine. > [!NOTE] > You can find the Docker container for any space on the hub. > > * Open the space page on the hub. > * Click the **⋮ (three dots)** menu. > * Select **“Run locally.”** > * Copy and execute the provided command in your terminal. > > ![open_env_launch_docker](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/open_env_launch_docker.png) > [!NOTE] > You can also use the **Docker option** with `from_docker_image` by providing the image name.. > For more details, refer to the official [OpenEnv documentation](https://meta-pytorch.org/OpenEnv/core/). **Connect to a remote Hugging Face Space** You can connect to a hosted environment running on the Hugging Face Hub by passing the URL of the space to the `base_url` parameter of the environment class. ```python env = EchoEnv(base_url="https://openenv-echo-env.hf.space") ``` > [!NOTE] > You can find the connection URL of any space on the hub. > > * Open the space page on the hub. > * Click the **⋮ (three dots)** menu. > * Select **“Embed this Space.”** > * Copy the connection URL. > [!WARNING] > **Currently**, it is recommended to **duplicate the Space to your own account** to avoid potential concurrency issues. **Local Python process** You can start the server manually as a local Python process. For more details about the available environments, refer to the [OpenEnv catalog](https://meta-pytorch.org/OpenEnv/environments/). ```bash hf download openenv/echo_env --repo-type=space --local-dir=echo_env python -m uvicorn echo_env.src.envs.echo_env.server.app:app --host 0.0.0.0 --port 8001 ``` And then you can connect to the environment using the following code: ```python env = EchoEnv(base_url="http://0.0.0.0:8001") ``` ## Environments Catalog Environment development is active and evolving. The best way to explore the **current catalog of maintained environments** is by visiting the official OpenEnv [catalog](https://huggingface.co/collections/openenv/environment-hub). Custom environments are also supported. To learn how to create your own, check out the guide on [Building Your Own Environment with OpenEnv](https://meta-pytorch.org/OpenEnv/environment-builder/). Environments are tightly integrated with the Hub, allowing you to **push new environments directly** so the community can easily pull, reuse, and adapt them for their own use cases. ## A simple example > [!NOTE] > You can explore more ready-to-use example scripts in the [`examples/scripts/openenv/`](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/) directory. The [echo.py](https://github.com/huggingface/trl/blob/main/examples/scripts/openenv/echo.py) script demonstrates a minimal, end-to-end integration between TRL and OpenEnv. In this example, the [Echo environment](https://meta-pytorch.org/OpenEnv/environments/echo/) rewards completions based on their text length, encouraging the model to generate longer outputs. This pattern can be extended to any custom environment that provides structured feedback or task-based rewards: ```python from echo_env import EchoEnv, EchoAction from trl import GRPOConfig, GRPOTrainer from trl.experimental.openenv import generate_rollout_completions # Create HTTP client for Echo Environment client = EchoEnv.from_hub("openenv/echo-env") """ Alternatively, you can start the environment manually with Docker and connect to it: # Step 1: Start the Echo environment docker run -d -p 8001:8001 registry.hf.space/openenv-echo-env:latest # Step 2: Connect the client to the running container client = EchoEnv(base_url="http://0.0.0.0:8001") """ def rollout_func(prompts: list[str], trainer: GRPOTrainer): # 1. Generate completions using TRL's helper (works for colocated vLLM) outputs = generate_rollout_completions(trainer, prompts) tokenizer = trainer.processing_class completions_text = [ tokenizer.decode(out["completion_ids"], skip_special_tokens=True) for out in outputs ] # 2. Step through the environment to get rewards client.reset() env_rewards = [] for msg in completions_text: env_result = client.step(EchoAction(message=msg)) env_rewards.append(env_result.reward) # 3. Add environment rewards as extra field return { "prompt_ids": [out["prompt_ids"] for out in outputs], "completion_ids": [out["completion_ids"] for out in outputs], "logprobs": [out["logprobs"] for out in outputs], "env_reward": env_rewards, } def reward_from_env(completions, **kwargs): """Extract environment rewards passed via rollout_func kwargs.""" env_rewards = kwargs.get("env_reward", []) return [float(reward) for reward in env_rewards] if env_rewards else [0.0] * len(completions) dataset = Dataset.from_dict({"prompt": ["You are an AI that interacts with an *Echo* environment. Word to echo:"] * 64}) # Setup trainer with custom rollout trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", reward_funcs=reward_from_env, train_dataset=dataset, rollout_func=rollout_func, # Use custom rollout args=GRPOConfig( use_vllm=True, vllm_mode="colocate", # Use colocate mode (default) num_train_epochs=1, num_generations=8, max_completion_length=2048, per_device_train_batch_size=8, gradient_accumulation_steps=4, ), ) trainer.train() ``` That's it! Now that you've seen the full example, let's unpack how the main pieces fit together. 1. **Environment Client:** `EchoEnv` implements an HTTP interface to interact with the environment server. 2. **Custom rollout:** The `rollout_func` generates completions and steps through the environment to collect rewards. 3. **Extra fields:** The rollout adds `env_reward` to the result dictionary, which is automatically passed to reward functions. 4. **Reward function:** Extracts `env_reward` from `kwargs` to apply environment-computed rewards during training. > [!TIP] > The trainer-aware rollout hook works in both vLLM server and colocate modes. Use `trl.experimental.openenv.generate_rollout_completions` so you reuse TRL's sampling configuration automatically. ### Running the Example You can run the example in either colocate mode (1 GPU) or server mode (2 GPUs): **Colocate mode (1 GPU, recommended)** ```bash python examples/scripts/openenv/echo.py --env-mode space --env-host https://openenv-echo-env.hf.space --vllm-mode colocate ``` This runs vLLM in the same process as training, requiring only a single GPU. **Server mode (2+ GPUs, scalable)** ```bash # Terminal 1: Start vLLM inference server CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen2.5-0.5B-Instruct --host 0.0.0.0 --port 8000 # Terminal 2: Run GRPO training with OpenEnv CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/echo.py --env-mode space --env-host https://openenv-echo-env.hf.space --vllm-mode server --vllm-server-url http://localhost:8000 ``` This runs vLLM as a separate server process, useful when you want to: - Share the inference server across multiple training jobs - Use multiple GPUs for the vLLM server (via `--tensor-parallel-size`) - Scale up training to many GPUs while sharing a single inference endpoint Alternatively, you can manually start the Echo environment in a Docker container before running the training: ```bash # Launch the Echo environment docker run -d -p 8001:8001 registry.hf.space/openenv-echo-env:latest # Run training with docker-local mode python examples/scripts/openenv/echo.py --env-mode docker-local --vllm-mode colocate ``` Below is the reward curve from training: ## Advanced Example Let's level this up a bit by training a model to interact with a more complex environment. We'll use the game word guessing game [wordle](https://www.nytimes.com/games/wordle/index.html) from the [`TextArena`](https://meta-pytorch.org/OpenEnv/environments/textarena/) environment. > [!NOTE] > You can explore the notebook version of this example [here](https://github.com/huggingface/trl/blob/main/examples/notebooks/openenv_wordle_grpo.ipynb). ### The TextArena Environment [TextArena](https://huggingface.co/papers/2504.11442) is an open-source collection of competitive text-based games designed to evaluate reasoning skills in LLMs using textual games like Wordle, Snake, Tic-Tac-Toe, and more. Research has shown that such games improve model performance on reasoning tasks. ![image of TextArena](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/text_arena_evals.png) We will use the `TextArena` environment to train a model to play Wordle. The environment is a simple text based response environment that allows the model to interact with the game by making guesses and receive feedback on them. ### Wordle Wordle is a useful game to train a model on because it requires the model to reason about the word and the feedback provided by the environment. Also, it is a purely language based game that requires no external tools or knowledge. Furthermore, we found that models from 1 billion parameters and up are able to improve on wordle and only require 8 tokens to generate a guess, which makes the game a good benchmark to experiment with Reinforcement Learning environments without significant compute requirements. > [!NOTE] How does Wordle work? > Wordle is a word guessing game where the player has to guess a 5-letter word. The player can make 6 guesses, and for each guess, the environment will provide feedback on the correctness of the guess. The player wins if they guess the word in 6 guesses or fewer. It challenges the model to generate words that are likely to be correct, and to learn from the feedback provided by the environment. > > For example, if the wordle environment returns the following feedback: > > ``` > G U E S S > X G Y X X > ``` > The model has guessed the word "GUESS" and the environment has provided feedback as the letters X, G, and Y. Referring to colors in the original game as blank, green, and yellow. From this feedback, the model should learn that the word "GUESS" is incorrect. The letter "E" is in the word, but in the wrong position. The letter "U" is correct and in the correct position. In the TextArena environment, a reward is only given when the model wins the game. The reward is 1.0 if the model wins, and 0.0 otherwise. This is not a very efficient reward signal for the model, so we have added a number of custom reward functions to the script to help the model learn to play the game. The extensible nature of `reward_funcs` and `rollout_func` allows you to add any custom reward function you want to the script. ### Rollout Function The rollout function runs one full Wordle episode, prompting the model for a guess each turn and capturing both environment rewards and auxiliary signals such as letter coverage and repetition penalties. ```python def rollout_once( trainer: GRPOTrainer, env: TextArenaEnv, tokenizer: AutoTokenizer, dataset_prompt: str, system_prompt: str, max_turns: int, ) -> dict[str, list]: result = env.reset() observation = result.observation prompt_ids: list[int] = [] completion_ids: list[int] = [] logprobs: list[float] = [] raw_rewards: list[float] = [] green_scores: list[float] = [] yellow_scores: list[float] = [] repetition_scores: list[float] = [] correct_scores: list[float] = [] guess_counts: dict[str, int] = {} for _turn in range(max_turns): # when the game is over the environment will return a done=True if result.done: break # set up the prompt for the model base_prompt = observation.prompt or dataset_prompt user_prompt = make_user_prompt(base_prompt, observation.messages) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] prompt_text = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=False, enable_thinking=False, ) # Generate completion using trainer (works for both colocate and server modes) rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0] prompt_ids.extend(rollout_outputs["prompt_ids"]) completion_ids.extend(rollout_outputs["completion_ids"]) logprobs.extend(rollout_outputs["logprobs"]) completion_text = rollout_outputs.get("text") or tokenizer.decode( rollout_outputs["completion_ids"], skip_special_tokens=True ) # extract the guess from the completion guess = extract_guess(completion_text) # step the environment with the guess result = env.step(TextArenaAction(message=guess)) raw_rewards.append(float(result.reward or 0.0)) observation = result.observation correct_score = float(result.reward or 0.0) feedback = extract_wordle_feedback(observation) # Update guess counts previous_occurrences = guess_counts.get(guess, 0) repetition_score = scale_repetition_score(previous_occurrences, len(guess_counts)) guess_counts[guess] = previous_occurrences + 1 # calculate custom reward signals from the feedback if not feedback: green_score = 0.0 yellow_score = 0.0 else: green_count, yellow_count = extract_feedback_counts(feedback) green_score = green_count / 5.0 yellow_score = yellow_count / 5.0 repetition_scores.append(repetition_score) green_scores.append(green_score) yellow_scores.append(yellow_score) correct_scores.append(correct_score) correct_reward_value = correct_scores[-1] if correct_scores else (raw_rewards[-1] if raw_rewards else 0.0) return { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "raw_rewards": raw_rewards, "correct_reward": correct_reward_value, "green_reward": green_scores[-1] if green_scores else 0.0, "yellow_reward": yellow_scores[-1] if yellow_scores else 0.0, "repetition_reward": repetition_scores[-1] if repetition_scores else 0.0, } ``` The environment has a reward signal based on the completion of the game. We found that most models struggle to ever win the game, so we have added a number of custom reward functions to the script to help the model learn to play the game more iteratively. At first, the model will learn to cover new letters and avoid repeating guesses. As it improves, it will learn to win the game. ### Reward Functions We log four reward streams that encourage the model to solve the puzzle, cover new letters, and avoid repeating guesses: - `reward_correct`: final win/loss signal from the environment. - `reward_greens`: density of green letters in the last feedback. - `reward_yellows`: density of yellow letters in the last feedback. - `reward_repetition`: penalty for guessing the same token multiple times. ```python def reward_correct(completions: List[str], **kwargs: Optional[Dict]) -> List[float]: rewards = kwargs.get("correct_reward") if kwargs else None return [float(r) for r in rewards] if rewards is not None else [0.0] * len(completions) def reward_greens(completions: List[str], **kwargs: Optional[Dict]) -> List[float]: rewards = kwargs.get("green_reward") if kwargs else None return [float(r) for r in rewards] if rewards is not None else [0.0] * len(completions) def reward_yellows(completions: List[str], **kwargs: Optional[Dict]) -> List[float]: rewards = kwargs.get("yellow_reward") if kwargs else None return [float(r) for r in rewards] if rewards is not None else [0.0] * len(completions) def reward_repetition(completions: List[str], **kwargs: Optional[Dict]) -> List[float]: rewards = kwargs.get("repetition_reward") if kwargs else None return [float(r) for r in rewards] if rewards is not None else [0.0] * len(completions) ``` ### Training the Model The training script wires the custom rollout and rewards into `GRPOTrainer`. The CLI exposes the configuration used during development as defaults, so you can override endpoints or hyperparameters at launch time. ```python parser = argparse.ArgumentParser() # ... add CLI arguments with sensible defaults ... cli_args = parser.parse_args() trainer = GRPOTrainer( model=cli_args.model_id, processing_class=tokenizer, reward_funcs=[ reward_correct, reward_greens, reward_yellows, reward_repetition, ], train_dataset=dataset, args=grpo_config, rollout_func=lambda prompts, trainer: rollout_func( env=env, tokenizer=tokenizer, prompts=prompts, trainer=trainer, cli_args=cli_args, system_prompt=system_prompt, ), ) trainer.train() ``` ### Running the Advanced Example You can run the Wordle example in either colocate mode (1 GPU) or server mode (2 GPUs): **Colocate mode (1 GPU, recommended)** ```bash python examples/scripts/openenv/wordle.py --vllm-mode colocate ``` This runs vLLM in the same process as training, requiring only a single GPU. **Server mode (2+ GPUs, scalable)** ```bash # Terminal 1: Start vLLM inference server CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen3-1.7B --host 0.0.0.0 --port 8000 # Terminal 2: Run GRPO training with OpenEnv CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/wordle.py --vllm-mode server --vllm-server-url http://localhost:8000 ``` This runs vLLM as a separate server process, useful when you want to: - Share the inference server across multiple training jobs - Use multiple GPUs for the vLLM server (via `--tensor-parallel-size`) - Scale up training to many GPUs while sharing a single inference endpoint You can also manually start the TextArena environment in a Docker container before running the training: ```bash # Launch the TextArena environment docker run -d -p 8001:8001 registry.hf.space/burtenshaw-textarena:latest ``` Then connect to it using `--env-mode docker-local--env-host localhost --env-port 8001`. ### Results The resulting model improves its performance on the game, both by reducing the number of repetitions and by increasing the number of correct guesses. However, the Qwen3-1.7B model we trained is not able to consistently win the game. The following reward curve shows the coverage of the model's guesses and the coverage of correct Y and G letters. We experimented with larger models like `gpt-oss-20b` and found that the model was able to consistently win the game. However, this requires a lot of compute to train the model. Why not try this out yourself? ================================================ FILE: docs/source/orpo_trainer.md ================================================ # ORPO Trainer [![model badge](https://img.shields.io/badge/All_models-ORPO-blue)](https://huggingface.co/models?other=orpo,trl) [![model badge](https://img.shields.io/badge/smol_course-Chapter_2-yellow)](https://github.com/huggingface/smol-course/tree/main/2_preference_alignment) ## Overview Odds Ratio Preference Optimization (ORPO) was introduced in [ORPO: Monolithic Preference Optimization without Reference Model](https://huggingface.co/papers/2403.07691) by [Jiwoo Hong](https://huggingface.co/JW17), [Noah Lee](https://huggingface.co/nlee-208), and [James Thorne](https://huggingface.co/j6mes). The abstract from the paper is the following: > While recent preference alignment algorithms for language models have demonstrated promising results, supervised fine-tuning (SFT) remains imperative for achieving successful convergence. In this paper, we study the crucial role of SFT within the context of preference alignment, emphasizing that a minor penalty for the disfavored generation style is sufficient for preference-aligned SFT. Building on this foundation, we introduce a straightforward and innovative reference model-free monolithic odds ratio preference optimization algorithm, ORPO, eliminating the necessity for an additional preference alignment phase. We demonstrate, both empirically and theoretically, that the odds ratio is a sensible choice for contrasting favored and disfavored styles during SFT across the diverse sizes from 125M to 7B. Specifically, fine-tuning Phi-2 (2.7B), Llama-2 (7B), and Mistral (7B) with ORPO on the UltraFeedback alone surpasses the performance of state-of-the-art language models with more than 7B and 13B parameters: achieving up to 12.20% on AlpacaEval_{2.0} (Figure 1), 66.19% on IFEval (instruction-level loose, Table 6), and 7.32 in MT-Bench (Figure 12). We release code and model checkpoints for Mistral-ORPO-alpha (7B) and Mistral-ORPO-beta (7B). It studies the crucial role of SFT within the context of preference alignment. Using preference data the method posits that a minor penalty for the disfavored generation together with a strong adaption signal to the chosen response via a simple log odds ratio term appended to the NLL loss is sufficient for preference-aligned SFT. Thus ORPO is a reference model-free preference optimization algorithm eliminating the necessity for an additional preference alignment phase thus saving compute and memory. The official code can be found in [xfactlab/orpo](https://github.com/xfactlab/orpo). This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif), [Lewis Tunstall](https://huggingface.co/lewtun) and [Alvaro Bartolome](https://huggingface.co/alvarobartt). ## Quick start This example demonstrates how to train a model using the ORPO method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model. We use the preference data from the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). You can view the data in the dataset here: Below is the script to train the model: ```python # train_orpo.py from datasets import load_dataset from trl.experimental.orpo import ORPOConfig, ORPOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") train_dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") training_args = ORPOConfig(output_dir="Qwen2-0.5B-ORPO") trainer = ORPOTrainer(model=model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_orpo.py ``` Distributed across 8 GPUs, the training takes approximately 30 minutes. You can verify the training progress by checking the reward graph. An increasing trend in the reward margin indicates that the model is improving and generating better responses over time. ![orpo qwen2 reward margin](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/orpo-qwen2-reward-margin.png) To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-ORPO) performs, you can use the [Transformers Chat CLI](https://huggingface.co/docs/transformers/quicktour#chat-with-text-generation-models).
$ transformers chat trl-lib/Qwen2-0.5B-ORPO
<quentin_gallouedec>:
What is the best programming language?

<trl-lib/Qwen2-0.5B-ORPO>:
It's challenging to determine the best programming language as no one language is perfect, as the complexity of a task and the type of project are significant factors. Some popular languages include Java, Python, JavaScript, and
C++. If you have specific needs or requirements for a specific project, it's important to choose the language that best suits those needs.

Here are some other factors to consider when choosing a programming language for a project:

 • Language proficiency: A good programming language is more likely to be easy to understand and use, and will allow developers to collaborate on projects more efficiently.
 • Ease of use: There are tools and libraries available to make programming more accessible, so developers should choose a language that can help them get started easier.
 • Code readability: A clear and concise codebase should be easy to read and understand, especially when working with large projects.
 • Tool and framework support: There are numerous libraries available for Python, Java, and JavaScript, along with tools like IDEs and static code analysis tools.
 • Accessibility: Some languages and tools have features that make them more accessible to developers with disabilities, such as support for screen readers.
 • Version control: As your projects grow and complexity increases, version control tools can be beneficial for tracking changes.

## Expected dataset type ORPO requires a [preference dataset](dataset_formats#preference). The [`experimental.orpo.ORPOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset format. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. Although the [`experimental.orpo.ORPOTrainer`] supports both explicit and implicit prompts, we recommend using explicit prompts. If provided with an implicit prompt dataset, the trainer will automatically extract the prompt from the `"chosen"` and `"rejected"` columns. For more information, refer to the [preference style](dataset_formats#preference) section. ## Example script We provide an example script to train a model using the ORPO method. The script is available in [`examples/scripts/orpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/orpo.py) To test the ORPO script with the [Qwen2 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/trl-lib/ultrafeedback_binarized), run the following command: ```bash accelerate launch examples/scripts/orpo.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --num_train_epochs 1 \ --output_dir Qwen2-0.5B-ORPO ``` ## Usage tips ### For Mixture of Experts Models: Enabling the auxiliary loss MOEs are the most efficient if the load is about equally distributed between experts. To ensure that we train MOEs similarly during preference-tuning, it is beneficial to add the auxiliary loss from the load balancer to the final loss. This option is enabled by setting `output_router_logits=True` in the model config (e.g. [`~transformers.MixtralConfig`]). To scale how much the auxiliary loss contributes to the total loss, use the hyperparameter `router_aux_loss_coef=...` (default: `0.001`) in the model config. ## Logged metrics While training and evaluating, we record the following reward metrics: - `rewards/chosen`: the mean log probabilities of the policy model for the chosen responses scaled by beta - `rewards/rejected`: the mean log probabilities of the policy model for the rejected responses scaled by beta - `rewards/accuracies`: mean of how often the chosen rewards are > than the corresponding rejected rewards - `rewards/margins`: the mean difference between the chosen and corresponding rejected rewards - `log_odds_chosen`: the mean log odds ratio of the chosen responses over the rejected responses - `log_odds_ratio`: the mean of the `log(sigmoid(log_odds_chosen))` - `nll_loss`: the mean negative log likelihood loss from the SFT part of the loss over chosen responses ## ORPOTrainer [[autodoc]] experimental.orpo.ORPOTrainer - train - save_model - push_to_hub ## ORPOConfig [[autodoc]] experimental.orpo.ORPOConfig ================================================ FILE: docs/source/paper_index.md ================================================ # Paper Index ## Group Relative Policy Optimization Papers relating to the [`GRPOTrainer`]. ### DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models **📜 Paper**: https://huggingface.co/papers/2402.03300 Introduces Group Relative Policy Optimization (GRPO) and shows strong math-reasoning gains from math-centric pretraining plus group-relative PPO-style optimization. Used in TRL via [`GRPOTrainer`]. ```python from trl import GRPOConfig, GRPOTrainer # The paper doesn't specify its hyperparameters, so here we provide hyperparameters from "DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement learning" instead. training_args = GRPOConfig( loss_type="grpo", beta=0.001, # "the KL coefficient to 0.001" epsilon=10.0, # "the GRPO clip ratio ϵ to 10" num_generations=16, # "For each question, we sample 16 outputs..." max_completion_length=32_768, # "...with a maximum length of 32,768" steps_per_generation=16, # "To accelerate training, each rollout generates 8,192 outputs, which are randomly split into 16 minibatches" # "resulting in a training batch size of 512". One way to achieve this setting with 1 device is per_device_train_batch_size=4, gradient_accumulation_steps=128 per_device_train_batch_size=4, gradient_accumulation_steps=128, ) trainer = GRPOTrainer( ..., args=training_args, ) ``` ### DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning **📜 Paper**: https://huggingface.co/papers/2501.12948 DeepSeek-R1 achieves reasoning performance comparable to OpenAI-o1 through a multi-stage pipeline that transitions from pure reinforcement learning (RL) to a refined, human-aligned model. Unlike its predecessor, DeepSeek-R1-Zero, which used pure RL on a base model, R1 follows a structured four-stage evolution: 1. Cold Start: The base model is fine-tuned on a small set of high-quality, long Chain-of-Thought (CoT) data to provide a stable starting point. 2. Reasoning-Oriented RL: Large-scale RL is applied to enhance performance in math, coding, and logic, using rule-based rewards and a language consistency reward to reduce language mixing. 3. Rejection Sampling & SFT: The RL checkpoint generates 600k reasoning samples via rejection sampling, which are combined with 200k non-reasoning (general) samples to create a new dataset for a second round of Supervised Fine-Tuning. 4. RL for all Scenarios: A final RL stage aligns the model with human preferences (helpfulness and harmlessness) across all domains while maintaining reasoning strength. Distillation: Empowering Small Models A key contribution of the paper is demonstrating that reasoning patterns can be distilled from a large model (DeepSeek-R1) into smaller dense models (e.g., Qwen and Llama series). Distillation was found to be more effective for small models than training them with pure RL from scratch. You can use the GRPOTrainer to replicate the reasoning-heavy stages of this pipeline. ```python from trl import GRPOConfig, GRPOTrainer # Example configuration for a reasoning-oriented GRPO stage # Based on the Open-R1 recipe for Qwen-7B training_args = GRPOConfig( learning_rate=4.0e-5, max_prompt_length=4096, max_completion_length=32768, # Support for long Chain-of-Thought num_generations=16, # Sample 16 outputs per prompt for group relative advantage beta=0.001, # KL coefficient use_vllm=True, # Use vLLM backend for accelerated rollout generation ) trainer = GRPOTrainer( model=model, args=training_args, train_dataset=dataset, reward_funcs=[accuracy_reward, format_reward], # R1-Zero used rule-based rewards ) trainer.train() ``` ### Group Sequence Policy Optimization **📜 Paper**: https://huggingface.co/papers/2507.18071 GSPO is a GRPO variant that computes importance sampling weights at the sequence level instead of per-token. To reproduce the paper's setting, use this configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( importance_sampling_level="sequence", loss_type="grpo", beta=0.0, # GSPO set KL regularization to zero: https://github.com/volcengine/verl/pull/2775#issuecomment-3131807306 epsilon=3e-4, # GSPO paper (v2), section 5.1 epsilon_high=4e-4, # GSPO paper (v2), section 5.1 gradient_accumulation_steps=1, steps_per_generation=4, # partition rollout batch into 4 mini-batches. GSPO paper (v2), section 5.1. Must be 4 times gradient_accumulation_steps ) ``` Note that this method only has an effect when training goes slightly off-policy—for example, when `steps_per_generation > gradient_accumulation_steps` or `num_iterations > 1`. Otherwise, it is effectively equivalent to no modification. TRL also provide an experimental implementation of GSPO-token, see [Experimental - GSPO-Token](experimental#gspo-token). #### Policy ratio: GRPO vs. GSPO In GSPO, the policy ratio is defined at the sequence-level. In other words, it is the ratio between the probability of the current policy generating a sequence over the old policy generating that same sequence. The sequence likelihood is defined as: $$ \pi_\theta (o_i | q) = \prod_{t=1}^{|o_i|} \pi_\theta (o_{i,t} | q, o_{i, < t} ), $$ where \\( \pi_\theta \\) is the policy \\( \pi \\) with parameters \\(\theta\\), \\( o_i \\) is the \\( i \\)-th output sequence \\( o \\) and \\(o_{i,t}\\) is the \\( t \\)-th token in this sequence, \\( q \\) is the input query. The sequence likelihood ratio \\( s_i (\theta) \\) is defined as: $$ s_i (\theta) = \left(\frac{\pi_\theta (o_i | q)}{\pi_{\theta_{old}} (o_i | q)} \right)^{\frac{1}{|o_i|}} $$ The exponent \\( \frac{1}{|o_i|} \\) represents a sequence-length normalization, minimizing the influence of sequence length in sequence likelihood. In other terms, it computes the geometric mean of token probabilities, ensuring a fair comparison across sequences of varying lengths. While GSPO defines the policy ratio at the sequence level, GRPO operates at the token level. Specifically, GRPO computes an importance ratio for each token in the sequence: $$ w_{i,t}(\theta) = \frac{\pi_\theta (o_{i,t} | q, o_{i,< t})}{\pi_{\theta_{\text{old}}} (o_{i,t} | q, o_{i,< t})} $$ This token-level ratio is then combined with a shared advantage \\( \hat{A}_i \\), and the GRPO objective clips and optimizes each token independently across the sequence. ### DAPO: An Open-Source LLM Reinforcement Learning System at Scale **📜 Paper**: https://huggingface.co/papers/2503.14476 The DAPO algorithm includes 5 key components: - Overlong Filtering - Clip-Higher - Soft Overlong Punishment - Token-level Loss - Dynamic Sampling (⚠️ Not supported in TRL) To reproduce the paper's setting, use this configuration: ```python from trl import GRPOConfig, GRPOTrainer training_args = GRPOConfig( # Overlong Filtering mask_truncated_completions=True, # Token-level Loss loss_type="dapo", # Clip-Higher epsilon_high=0.28, # DAPO paper: section 4.1 epsilon=0.2, # DAPO paper: section 4.1 # Other parameters used per_device_train_batch_size=512, # mini-batch size for training in the paper, DAPO paper: section 4.1 num_generations=16, # number of sample responses in the paper, DAPO paper: section 4.1 max_completion_length=20480, # maximum number of tokens for generation in the paper, DAPO paper: section 4.1 beta=0.0, # section 2.3, DAPO paper ) # Soft Overlong Punishment sop_reward = get_soft_overlong_punishment(max_completion_len=20480, soft_punish_cache=4096) # DAPO paper: section 4.1 trainer = GRPOTrainer( ..., args=training_args, reward_funcs=[..., sop_reward], ) ``` ### INTELLECT-2: A Reasoning Model Trained Through Globally Decentralized Reinforcement Learning **📜 Paper**: https://huggingface.co/papers/2505.07291 INTELLECT-2 is the first globally distributed reinforcement learning training run of a 32 billion parameter language model using fully asynchronous RL across a dynamic, heterogeneous swarm of permissionless compute contributors. The authors propose modifications to the standard GRPO training recipe, including two-sided GRPO clipping for increased training stability. To reproduce the paper's setting, use this configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( delta=4, # δ in section 4.1 of the paper epsilon=0.2, # ε in section 4.1 of the paper beta=0.001, # KL divergence coefficient in section 4.1 of the paper num_generations=16, # responses per prompt in section 4.1 of the paper learning_rate=3e-7, # section 4.1 of the paper ) ``` ### Beyond the 80/20 Rule: High-Entropy Minority Tokens Drive Effective Reinforcement Learning for LLM Reasoning **📜 Paper**: https://huggingface.co/papers/2506.01939 A minority of tokens with high entropy act as reasoning "forks" in the CoT path, driving exploration and performance gains for RLVR, while low-entropy majority tokens contribute little or even impede learning. RLVR mainly adjusts high-entropy tokens, largely preserving the base model’s overall entropy patterns. Thus landing on the 80/20 rule, training on only 20% of the tokens with the highest entropy is comparable or supasses full-gradient updates for Qwen3 models. The paper's main results use vanilla DAPO (⚠️ Dynamic Sampling is not supported in TRL). To replicate the main results, use the following configuration: ```python from trl import GRPOConfig, GRPOTrainer from trl.rewards import get_soft_overlong_punishment training_args = GRPOConfig( # --- vanilla DAPO parameters (80/20 rule: section 5.2) --- # # Overlong Filtering mask_truncated_completions=True, # Token-level Loss loss_type="dapo", # Clip-Higher epsilon_high=0.28, # DAPO paper: section 4.1 epsilon=0.2, # DAPO paper: section 4.1 # Other parameters used per_device_train_batch_size=512, # mini-batch size for training in the paper, DAPO paper: section 4.1 num_generations=16, # number of sample responses in the paper, DAPO paper: section 4.1 max_completion_length=20480, # maximum number of tokens for generation in the paper, DAPO paper: section 4.1 beta=0.0, # section 2.3, DAPO paper # --- Gradients on the highest entropy tokens --- # top_entropy_quantile=0.2 ) # Soft Overlong Punishment sop_reward = get_soft_overlong_punishment(max_completion_len=20480, soft_punish_cache=4096) # DAPO paper: section 4.1 trainer = GRPOTrainer( ..., args=training_args, reward_funcs=[..., sop_reward], ) ``` ### Dr. GRPO: Understanding R1-Zero-Like Training: A Critical Perspective **📜 Paper**: https://huggingface.co/papers/2503.20783 A study of R1-Zero training identifies pretraining effects on RL performance and proffers Dr. GRPO to enhance token efficiency, achieving superior accuracy on AIME 2024. To reproduce the paper's setting, use this configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( loss_type="dr_grpo", per_device_train_batch_size=1, # train_batch_size_per_device in the Training section of the repository num_generations=8, # num_samples in the Training section of the repository max_completion_length=3000, # generate_max_length in the Training section of the repository beta=0.0, # β in the Training section of the repository ) ``` ### Part I: Tricks or Traps? A Deep Dive into RL for LLM Reasoning (Lite PPO) **📜 Paper**: https://huggingface.co/papers/2508.08221 The authors of this paper find that the combination of: 1. scaling rewards by the standard deviation computed over the entire batch and 2. aggregating loss over the total number of tokens can unlock the learning capability of critic-free policies using vanilla PPO loss. Their results demonstrate that this simple combination consistently improves performance, surpassing strategies like GRPO and [DAPO](https://huggingface.co/papers/2503.14476). TRL supports using these learnings to train a GRPO model by: ```python from trl import GRPOConfig training_args = GRPOConfig( ... scale_rewards="batch", loss_type="dapo", # Other parameters used beta=0.0, # = init_kl_coef in the paper top_p=0.99, top_k=100, temperature=0.99, num_generations=8, # = num_return_sequences in the paper num_iterations=1, # = ppo_epochs in the paper per_device_train_batch_size=4, gradient_accumulation_steps=32, steps_per_generation=8, # (rollout_batch_size*num_return_sequences) / (per_device_train_batch_size*gradient_accumulation_steps) ) ``` Note that when using gradient accumulation, the loss is aggregated over the total number of tokens in the batch, but not over the accumulated batch. For more details, see the [GRPO Trainer - Loss types](grpo_trainer#loss_types). ### Truncated Importance Sampling **📰 Blog**: https://fengyao.notion.site/off-policy-rl Online policy learning methods commonly use an optimized inference framework for rollout generation (e.g vLLM) that is separate from the training backend. This introduces a rollout-training mismatch, exemplified in the following PPO objective: $$ \small{ \mathbb{E}_{a\sim\textcolor{red}{\pi_{\text{inference}}}(\theta_{\mathrm{old}})} \Bigl[ \min\Bigl( \frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})}\,\hat A, \;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\,1-\epsilon,\,1+\epsilon\bigr)\,\hat A \Bigr) \Bigr] } $$ Despite \\( \textcolor{red}{\pi_{\text{inference}}} \\) and \\( \textcolor{blue}{\pi_{\text{training}}} \\) sharing the same model parameters \\( \theta \\), they can produce significantly different token probabilities. This unexpected behavior implicitly breaks the on-policy assumption, and silently turns training off-policy. Truncated Importance Sampling (TIS) addresses this issue by adapting the model update via importance-sampling correction. The gradient computation of the aforementioned PPO objective becomes $$ \small{ \mathbb{E}_{a\sim\textcolor{red}{\pi_{\text{inference}}}(\theta_{\mathrm{old}})} \Bigl[ \underbrace{\min(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})}{\textcolor{red}{\pi_{\text{inference}}}(a, \theta_{\mathrm{old}})}, C)}_{\text{truncated importance ratio}} \cdot \nabla_\theta \min\Bigl( \frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})}\,\hat A, \;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\,1-\epsilon,\,1+\epsilon\bigr)\,\hat A \Bigr) \Bigr] } $$ where \\( C \\) is a hyper-parameter. TIS is implemented in GRPO, and is enabled by selecting a `vllm_importance_sampling_mode` variant that includes the term `truncate`, such as `"sequence_truncate"` or `"token_truncate"`. ```python from trl import GRPOConfig training_args = GRPOConfig( ... use_vllm=True, vllm_importance_sampling_correction=True, # default True vllm_importance_sampling_mode="sequence_truncate", # or "token_truncate" vllm_importance_sampling_cap=2.0, # hyper-parameter C ) ``` ### Masked Importance Sampling **📰 Blog**: https://ringtech.notion.site/icepop **📰 Blog**: https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda Masked Importance Sampling (MIS) addresses the same issue as [Truncated Importance Sampling](#truncated-importance-sampling) but replaces clipping with masking. MIS takes a more decisive stance by discarding updates whose discrepancy exceeds a threshold \\( C \\). We apply upper-side masking, so any ratio above \\( C \\) is removed from the update. $$ \small{ \mathbb{E}_{a\sim\textcolor{red}{\pi_{\text{inference}}}(\theta_{\mathrm{old}})} \Bigl[ \underbrace{\mathbf{1}\left[ \frac{\pi_{\text{training}}(a, \theta_{\mathrm{old}})} {\pi_{\text{inference}}(a, \theta_{\mathrm{old}})} \le C \right] \cdot \frac{\pi_{\text{training}}(a, \theta_{\mathrm{old}})} {\pi_{\text{inference}}(a, \theta_{\mathrm{old}})}}_{\text{masked importance ratio}} \cdot \nabla_\theta \min\Bigl( \frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})}\,\hat A, \;\mathrm{clip}\bigl(\frac{\textcolor{blue}{\pi_{\text{training}}}(a, \theta)}{\textcolor{blue}{\pi_{\text{training}}}(a, \theta_{\mathrm{old}})},\,1-\epsilon,\,1+\epsilon\bigr)\,\hat A \Bigr) \Bigr] } $$ MIS is implemented for GRPO, and is enabled by selecting a `vllm_importance_sampling_mode` variant that includes the term `"mask"`, such as `"sequence_mask"` or `"token_mask"`. ```python from trl import GRPOConfig training_args = GRPOConfig( ... use_vllm=True, vllm_importance_sampling_correction=True, # default True vllm_importance_sampling_mode="sequence_mask", # or "token_mask" vllm_importance_sampling_cap=2.0, # hyper-parameter C ) ``` ### Sequence-level Importance Sampling **📰 Blog**: https://yingru.notion.site/When-Speed-Kills-Stability-Demystifying-RL-Collapse-from-the-Training-Inference-Mismatch-271211a558b7808d8b12d403fd15edda The theoretically principled way to correct for the training-inference distribution shift is importance sampling, as introduced in the two papers above [Truncated Importance Sampling](#truncated-importance-sampling) and [Masked Importance Sampling](#masked-importance-sampling). However, the choice of formulation is crucial for keeping the gradient unbiased and ensuring stable training. This work shows that sequence-level importance sampling is the sound approach for addressing the training–inference mismatch. Although token-level importance sampling achieves lower variance than a sequence-level ratio, it introduces bias and is therefore argued to be unsuitable for autoregressive models. The token-level gradient estimator is $$ \mathbb{E}_{x\sim\mathcal{D},\, y\sim \pi^{\text{inference}}_\theta(\cdot|x)} \Bigg[ R(x,y)\,\cdot\, \sum_{t=0}^{|y|-1} \frac{\pi^{\text{training}}_\theta(y_t\,|\,x, y_{ 0`: ```python from trl import GRPOConfig training_args = GRPOConfig( ..., beta=0.001, # the paper doesn't specify the value used, so we use the value from "DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement learning" use_bias_correction_kl=True, ) ``` - The **Off-Policy Masking**, which stabilizes training by ignoring sequences where the policy performs poorly (negative advantage) **and** has drifted significantly from the old policy (high KL divergence). The off-policy binary mask \\(\textcolor{red}{M_{i,t}}\\) is defined as: $$ \textcolor{red}{M_{i,t}} = \begin{cases} 0 & \text{if } \hat{A}_{i,t} < 0 \quad \text{and} \quad \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} \log \frac{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i, \textcolor{blue}{\delta} \\ 1 & \text{otherwise} \end{cases} $$ This mask is then applied to the GRPO loss as follows: $$ \mathcal{L}_{\text{GRPO}}(\theta) = -\frac{1}{G} \sum_{i=1}^G \frac{1}{|o_i|} \sum_{t=1}^{|o_i|} \left[ \min \left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})} \hat{A}_{i,t}, \, \text{clip}\left( \frac{\pi_\theta(o_{i,t} \mid q, o_{i,< t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,< t})}, 1 - \epsilon, 1 + \epsilon \right) \hat{A}_{i,t} \right) \textcolor{red}{M_{i,t}} - \beta \mathbb{D}_{\text{KL}}\left[\pi_\theta \| \pi_{\text{ref}}\right] \right] $$ To enable this feature, use the `off_policy_mask_threshold` (corresponding to \\( \textcolor{blue}{\delta} \\)) in the [`GRPOConfig`]: ```python from trl import GRPOConfig training_args = GRPOConfig( ..., off_policy_mask_threshold=0.5, ) ``` While the paper doesn't specify a \\( \textcolor{blue}{\delta} \\) value used, a good starting point could be \\( \textcolor{blue}{\delta} = 0.5 \\). If training seems too conservative or too many sequences are masked, you can increase the value. For reference, \\( \textcolor{blue}{\delta} = 1.0 \\) corresponds to an average log-ratio divergence of 1 nat per token, i.e. on sequences where this threshold is exceeded, the old policy was on average \\( e^1 \approx 2.7 \\) times more likely to generate these tokens than the current policy. ### GDPO: Group reward-Decoupled Normalization Policy Optimization for Multi-reward RL Optimization **📜 Paper**: https://huggingface.co/papers/2601.05242 GDPO is a reinforcement learning optimization method designed for multi-reward training. While existing approaches commonly apply Group Relative Policy Optimization (GRPO) in multi-reward settings, the authors show that this leads to reward advantages collapse, reducing training signal resolution and causing unstable or failed convergence. GDPO resolves this issue by decoupling reward normalization across individual rewards, preserving their relative differences and enabling more faithful preference optimization. To enable GDPO for multi-reward RL training, simply set: For a group of \\( N \\) rewards and \\( G \\) samples per group, GDPO normalizes each reward independently: $$ A_n^{(i,j)} = \frac{r_n^{(i,j)} - \text{mean}\{r_n^{(i,1)}, \ldots, r_n^{(i,G)}\}}{\text{std}\{r_n^{(i,1)}, \ldots, r_n^{(i,G)}\} + \epsilon} $$ The normalized group advantage is then aggregated across rewards: $$ A^{(i,j)} = \sum_{n=1}^{N} w_n A_n^{(i,j)} $$ The final per-batch normalization produces: $$ \hat{A}^{(i,j)} = \frac{A^{(i,j)} - \text{mean}_{i',j'}\{A^{(i',j')}\}}{\text{std}_{i',j'}\{A^{(i',j')}\} + \epsilon} $$ Here, \\( \text{mean}_{i',j'}\{A^{(i',j')}\} \\) and \\( \text{std}_{i',j'}\{A^{(i',j')}\} \\) denote statistics over all groups in the batch. ```python from trl import GRPOConfig training_args = GRPOConfig( ..., multi_objective_aggregation="normalize_then_sum", ) ``` Note that this method only has an effect when training involve more than one reward function. The authors provide a easy-to-use, slurm-free training example that enable the community to quickly validate GDPO’s effectiveness over GRPO, see [Experiment-"Aha" moment](https://github.com/NVlabs/GDPO/tree/main/trl-GDPO). ### Length-Unbiased Sequence Policy Optimization: Revealing and Controlling Response Length Variation in RLVR **📜 Paper**: https://huggingface.co/papers/2602.05261 Length-Unbiased Sequence Policy Optimization (LUSPO) modifies GSPO by scaling each sequence's loss by its length. This corrects GSPO's gradient bias that penalizes longer responses. To reproduce the paper's setting, use this configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( loss_type="luspo", importance_sampling_level="sequence", epsilon=2e-3, # section 5.1 of the paper epsilon_high=2.5e-3, # section 5.1 of the paper ) ``` ### VESPO: Variational Sequence-Level Soft Policy Optimization for Stable Off-Policy LLM Training **📜 Paper**: https://huggingface.co/papers/2602.10693 VESPO addresses training instability in off-policy RL caused by policy staleness, asynchronous updates, and train-inference mismatches. Rather than relying on heuristic token-level clipping (GRPO) or sequence-length normalization (GSPO), VESPO derives a principled reshaping kernel from a variational framework. In practice, this yields a smooth, asymmetric Gamma weighting function that gracefully suppresses extreme sequence-level importance weights without introducing length bias. $$ \mathcal{L}_{\text{VESPO}}(\theta) = - \mathbb{E}_{\tau \sim \mu} \left[ \underbrace{W(\tau)^{k} \cdot \exp\left(\lambda (1 - W(\tau))\right)}_{\phi(W) \text{ detached }} \cdot \mathcal{A}(\tau) \cdot \log \pi_\theta(\tau) \right] $$ with \\( W(\tau) = \frac{\pi_\theta(\tau)}{\mu(\tau)} \\) the sequence level importance ratio, and \\( \phi(W) \\) is detached from the computation graph to serve as a gradient scaling coefficient. ```python from trl import GRPOConfig training_args = GRPOConfig( loss_type="vespo", use_vllm=True, # or False if not using any token-level `vllm_importance_sampling_correction` methods vllm_importance_sampling_mode="token_truncate", # default correction mode for VESPO, `token_mask` also supported vespo_k_pos=2.0, # power exponent (c1 in paper Section 3.4) for positive advantages vespo_lambda_pos=3.0, # decay factor (c2 in paper Section 3.4) for positive advantages vespo_k_neg=3.0, # power exponent (c1 in paper Section 3.4) for negative advantages vespo_lambda_neg=2.0, # decay factor (c2 in paper Section 3.4) for negative advantages ) ``` ### Rethinking the Trust Region in LLM Reinforcement Learning **📜 Paper**: https://huggingface.co/papers/2602.04879 DPPO replaces PPO/GRPO's heuristic ratio-clipping with a principled trust region based on direct policy divergence estimates. PPO-style clipping masks tokens based on the probability ratio π/μ, which over-penalizes low-probability tokens and under-penalizes high-probability ones. DPPO instead masks based on direct approximations of policy divergence (TV or KL), ensuring updates stay within a theoretically grounded trust region. Four divergence approximations are supported: `binary_tv`, `binary_kl`, `topk_tv`, and `topk_kl`. ```python from trl.experimental.dppo import DPPOConfig, DPPOTrainer training_args = DPPOConfig( divergence_type="binary_tv", # divergence approximation divergence_topk=20, # K for top-K divergence modes (Section 7 / Appendix G.2 of the paper) epsilon=0.15, # δ_low threshold (Appendix F of the paper) epsilon_high=0.15, # δ_high threshold (Appendix F of the paper) clip_ratio_c=20.0, # IS ratio upper bound C (Section 5.4 of the paper) beta=0.0, # KL regularization coefficient use_vllm=True, ) trainer = DPPOTrainer( model="your-model", reward_funcs=[...], args=training_args, train_dataset=dataset, ) trainer.train() ``` The official code [sail-sg/Stable-RL](https://github.com/sail-sg/Stable-RL) ## Direct Policy Optimization Papers relating to the [`DPOTrainer`] ### Direct Preference Optimization: Your Language Model is Secretly a Reward Model **📜 Paper**: https://huggingface.co/papers/2305.18290 Direct Preference Optimization (DPO) fine-tunes language models more efficiently and with better performance compared to reinforcement learning from human feedback (RLHF), by directly optimizing policy training based on human preferences. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="sigmoid", # losses in Appendix B of the paper per_device_train_batch_size=64, # batch size in Appendix B of the paper learning_rate=1e-6, # learning rate in Appendix B of the paper beta=0.1, # β in Appendix B of the paper ) ``` ### SLiC-HF: Sequence Likelihood Calibration with Human Feedback **📜 Paper**: https://huggingface.co/papers/2305.10425 Sequence Likelihood Calibration (SLiC) is shown to be an effective and simpler alternative to Reinforcement Learning from Human Feedback (RLHF) for learning from human preferences in language models. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="hinge", # Section 2 of the paper per_device_train_batch_size=512, # batch size in Section 3.2 of the paper learning_rate=1e-4, # learning rate in Section 3.2 of the paper ) ``` These parameters only appear in the [published version](https://openreview.net/pdf?id=0qSOodKmJaN) ### Statistical Rejection Sampling Improves Preference Optimization **📜 Paper**: https://huggingface.co/papers/2309.06657 Proposes **RSO**, selecting stronger preference pairs via statistical rejection sampling to boost offline preference optimization; complements DPO/SLiC. They also introduce a new loss defined as: $$ \mathcal{L}_{\text{hinge-norm}}(\pi_\theta) = \mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ \max\left(0,\; 1 - \left[\gamma \log \frac{\pi_\theta(y_w \mid x)}{\pi_\text{ref}(y_w \mid x)} - \gamma \log \frac{\pi_\theta(y_l \mid x)}{\pi_\text{ref}(y_l \mid x)}\right]\right) \right] $$ To train with RSO-filtered data and the hinge-norm loss, you can use the following code: ```python from trl import DPOConfig, DPOTrainer dataset = ... def rso_accept(example): # replace with your actual filter/score logic return example["rso_keep"] train_dataset = train_dataset.filter(rso_accept) training_args = DPOConfig( loss_type="hinge", beta=0.05, # correspond to γ in the paper ) trainer = DPOTrainer( ..., args=training_args, train_dataset=train_dataset, ) trainer.train() ``` ### Beyond Reverse KL: Generalizing Direct Preference Optimization with Diverse Divergence Constraints **📜 Paper**: https://huggingface.co/papers/2309.16240 Proposes \(( f \\)-DPO, extending DPO by replacing the usual reverse-KL regularizer with a general \(( f \\)-divergence, letting you trade off mode-seeking vs mass-covering behavior (e.g. forward KL, JS, \(( \alpha \\)-divergences). The only change is replacing the DPO log-ratio margin with an **f′ score**: $$ \mathcal{L}_{f\text{-DPO}}(\pi_\theta) = \mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ -\log \sigma\left( \beta \textcolor{red}{f'}\textcolor{red}{\Big(}\frac{\pi_\theta(y_w|x)}{\pi_{\text{ref}}(y_w|x)}\textcolor{red}{\Big)} - \beta \textcolor{red}{f'}\textcolor{red}{\Big(}\frac{\pi_\theta(y_l|x)}{\pi_{\text{ref}}(y_l|x)}\textcolor{red}{\Big)} \right) \right] $$ Where \\( f' \\) is the derivative of the convex function defining the chosen \(( f \\)-divergence. To reproduce: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="sigmoid", beta=0.1, f_divergence_type="js_divergence", # or "reverse_kl" (default), "forward_kl", "js_divergence", "alpha_divergence" f_alpha_divergence_coef=0.5, # only used if f_divergence_type="alpha_divergence" ) ``` ### A General Theoretical Paradigm to Understand Learning from Human Preferences **📜 Paper**: https://huggingface.co/papers/2310.12036 Learning from human preferences can be written as a single KL-regularized objective over pairwise preference probabilities, $$ \max_\pi ;\mathbb{E}\big[\Psi\left(p^*(y \succ y' \mid x)\right)\big] - \tau\mathrm{KL}(\pi||\pi_{\text{ref}}), $$ which reveals RLHF and DPO as special cases corresponding to the logit choice of \\( \Psi \\). The paper shows that this logit transform amplifies near-deterministic preferences and effectively weakens KL regularization, explaining overfitting. Using the **Identity transform (IPO)** avoids this pathology by optimizing preferences directly, without assuming a Bradley–Terry reward model. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="ipo", # Section 5.1 of the paper per_device_train_batch_size=90, # mini-batch size in Section C.1 of the paper learning_rate=1e-2, # learning rate in Section C.1 of the paper ) ``` These parameters only appear in the [published version](https://proceedings.mlr.press/v238/gheshlaghi-azar24a/gheshlaghi-azar24a.pdf) ### Towards Efficient and Exact Optimization of Language Model Alignment **📜 Paper**: https://huggingface.co/papers/2402.00856 The paper shows that direct preference methods like DPO optimize the wrong KL direction, leading to blurred preference capture, and proposes EXO as an efficient way to exactly optimize the human‑preference alignment objective by leveraging reverse KL probability matching rather than forward KL approximations. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="exo_pair", # Section 3.2 of the paper # From Section B of the paper per_device_train_batch_size=64, learning_rate=1e-6, beta=0.1, ) ``` ### Noise Contrastive Alignment of Language Models with Explicit Rewards **📜 Paper**: https://huggingface.co/papers/2402.05369 The paper reframes language-model alignment as a *noise-contrastive classification* problem, proposing InfoNCA to learn a policy from explicit rewards (or preferences) by matching a reward-induced target distribution over responses, and showing DPO is a special binary case. It then introduces NCA, which adds an absolute likelihood term to prevent the likelihood collapse seen in purely relative (contrastive) objectives. With pairwise preferences, treat the chosen/rejected \\( K=2 \\), define scores \\( r=\beta(\log\pi_\theta-\log\pi_{\text{ref}}) \\), and apply the NCA preference loss \\( -\log\sigma(r_w)-\tfrac12\log\sigma(-r_w)-\tfrac12\log\sigma(-r_l) \\). To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="nca_pair", # From Section C of the paper per_device_train_batch_size=32, learning_rate=5e-6, beta=0.01, ) ``` ### Provably Robust DPO: Aligning Language Models with Noisy Feedback **📜 Paper**: https://huggingface.co/papers/2403.00409 DPO breaks under noisy human preferences because label flips bias the objective. Robust DPO fixes this by analytically debiasing the DPO loss under a simple noise model, with provable guarantees. $$ \mathcal{L}_{\text{robust}}(\pi_\theta) = \frac{(1-\varepsilon)\mathcal{L}_{\text{DPO}}(y_w, y_l) - \varepsilon\mathcal{L}_{\text{DPO}}(y_l, y_w)} {1-2\varepsilon} $$ Where \\( \mathcal{L}_{\text{DPO}} \\) is the DPO loss defined in [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](#direct-preference-optimization-your-language-model-is-secretly-a-reward-model) and \\( \varepsilon \\) is the probability of a label flip. This single correction turns noisy preference data into an unbiased estimator of the clean DPO objective. ```python from trl import DPOConfig training_args = DPOConfig( loss_type="robust", per_device_train_batch_size=16, # batch size in Section B of the paper learning_rate=1e-3, # learning rate in Section B of the paper beta=0.1, # β in Section B of the paper, max_length=512, # max length in Section B of the paper label_smoothing=0.1 # label smoothing $\varepsilon$ in Section 6 of the paper ) ``` ### Binary Classifier Optimization for Large Language Model Alignment **📜 Paper**: https://huggingface.co/papers/2404.04656 Theoretical analysis and a new algorithm, Binary Classifier Optimization, explain and enhance the alignment of large language models using binary feedback signals. To reproduce the paper's setting, use this configuration: BCO reframes language-model alignment as behavioral cloning from an optimal reward-weighted distribution, yielding simple supervised objectives that avoid RL while remaining theoretically grounded. It supports both unpaired reward data and pairwise preference data, with a reward-shift–invariant formulation that reduces to a DPO-style loss in the preference setting. For the pairwise preference setting, the BCO loss is defined as: $$ \mathcal{L}_{\text{bco\_pair}}(\pi_\theta) = \mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ -\log \sigma\Big( \beta[(\log\pi_\theta-\log\pi_{\text{ref}})(y_w) - (\log\pi_\theta-\log\pi_{\text{ref}})(y_l)] \Big) \right] $$ To reproduce the paper in this setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="bco_pair", # From Section C of the paper per_device_train_batch_size=128, learning_rate=5e-7, beta=0.01, ) ``` For the unpaired version, the user should utilize [`experimental.bco.BCOConfig`] and [`experimental.bco.BCOTrainer`]. ### Learn Your Reference Model for Real Good Alignment **📜 Paper**: https://huggingface.co/papers/2404.09656 Trust Region DPO (TR-DPO) updates the reference policy during training, demonstrating effectiveness against DPO on the Anthropic HH and TLDR datasets, outperforming DPO by up to 19% measured by automatic evaluation with GPT-4, improving coherence, correctness, level of detail, helpfulness, and harmlessness. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( sync_ref_model=True, # enable TR-DPO (Section 3 of the paper) ref_model_mixup_alpha=0.6, # α soft update weight (Table 1 of the paper) ref_model_sync_steps=512, # τ update frequency in steps (Table 1 of the paper) beta=0.05, # β temperature (Table 1 of the paper) learning_rate=1e-6, # learning rate (Table 2 of the paper) num_train_epochs=1, # Table 2 of the paper max_length=1024, # max tokens length (Table 2 of the paper) max_grad_norm=2, # max gradient norm (Table 2 of the paper) warmup_steps=100, # warm-up steps (Table 2 of the paper) ) ``` ### Iterative Reasoning Preference Optimization **📜 Paper**: https://huggingface.co/papers/2404.19733 Iterative RPO improves reasoning by repeatedly generating chain-of-thought candidates, building preference pairs from correct vs. incorrect answers, and training with a DPO + NLL objective. The extra NLL term is key for learning to actually generate winning traces. TRL can express the DPO + NLL objective by mixing `"sigmoid"` (DPO) with `"sft"` (NLL): ```python from trl import DPOConfig, DPOTrainer training_args = DPOConfig( loss_type=["sigmoid", "sft"], loss_weights=[1.0, 1.0], # alpha in the paper, recommended value is 1.0 ) trainer = DPOTrainer( ..., args=training_args, ) ``` Note that the paper uses an iterative loop: each iteration regenerates CoT candidates with the current model, then retrains on fresh preference pairs. TRL does not automate that loop for you. ### Self-Play Preference Optimization for Language Model Alignment **📜 Paper**: https://huggingface.co/papers/2405.00675 A self-play method called SPPO for language model alignment achieves state-of-the-art performance by approximating Nash equilibrium policy in a constant-sum game setting, outperforming other approaches with limited data. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="sppo_hard", # From Section 5 of the paper beta=0.001, # β = η^−1 per_device_train_batch_size=64, learning_rate=5e-7, ) ``` ### Provably Mitigating Overoptimization in RLHF: Your SFT Loss is Implicitly an Adversarial Regularizer **📜 Paper**: https://huggingface.co/papers/2405.16436 Regularized Preference Optimization (RPO) mitigates overoptimization in RLHF by fusing the DPO loss with the SFT loss, provably preventing the policy from choosing actions with spurious high proxy rewards. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type=["sigmoid", "sft"], # RPO loss = DPO + SFT (Section 5 of the paper) loss_weights=[1.0, 0.005], # η=0.005 SFT weight in Appendix E.1 of the paper beta=0.01, # β in Appendix E.1 of the paper learning_rate=5e-7, # learning rate in Appendix E.1 of the paper num_train_epochs=1, # Appendix E.1 of the paper ) ``` ### Distributional Preference Alignment of LLMs via Optimal Transport **📜 Paper**: https://huggingface.co/papers/2406.05882 Alignment via Optimal Transport (AOT) aligns large language models distributionally by penalizing violations of stochastic dominance between positive and negative sample distributions, achieving state-of-the-art performance on alignment benchmarks. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="aot", beta=0.01, # from the caption of Figure 2 ) ``` or, for the unpaired version: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="aot_unpaired", beta=0.01, # from the caption of Figure 2 ) ``` There is no additional hyperparameter in the paper. ### Discovering Preference Optimization Algorithms with and for Large Language Models **📜 Paper**: https://huggingface.co/papers/2406.08414 An LLM-driven method automatically discovers performant preference optimization algorithms, leading to a new algorithm called DiscoPOP that blends logistic and exponential losses. To reproduce the paper's setting, use this configuration: ```python from trl import DPOConfig training_args = DPOConfig( loss_type="discopop", per_device_train_batch_size=64, # batch size in Section B.1 of the paper learning_rate=5e-7, # learning rate in Section B.1 of the paper beta=0.05, # β in Section B.1 of the paper, discopop_tau=0.05 # τ in Section E of the paper ) ``` ### WPO: Enhancing RLHF with Weighted Preference Optimization **📜 Paper**: https://huggingface.co/papers/2406.11827 WPO reweights preference pairs by their policy probabilities to reduce the off-policy gap in DPO-style training. The loss is: $$ \mathcal{L}_{\text{WPO}} = -\mathbb{E}_{(x, y_w, y_l) \sim \mathcal{D}} \left[ \textcolor{red}{w(x, y_w) w(x, y_l)} \log p(y_w \succ y_l \mid x) \right] $$ where the weight \\( w(x, y) \\) is defined as: $$ w(x, y) = \exp\left(\frac{1}{|y|}\sum_{t=1}^{|y|} \log \frac{\pi_\theta(y_t \mid x, y_{ 0 (optimism coefficient) and β > 0 (KL regularization) in Algorithm 1 but does not specify numerical values. The following configuration uses TRL defaults: ```python from trl.experimental.xpo import XPOConfig training_args = XPOConfig( alpha=1e-5, # α exploration bonus weight, α ≥ 0 where α=0 reduces to online DPO (TRL default) beta=0.1, # β KL regularization coefficient (TRL default) ) ``` ## Distillation Papers relating to training a student model with the help of a teacher model. ### On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes **📜 Paper**: https://huggingface.co/papers/2306.13649 Introduces Generalized Knowledge Distillation (GKD), which addresses distribution mismatch in KD for auto-regressive models by training the student on its own generated outputs with teacher feedback, instead of a fixed set of sequences. GKD supports flexible loss functions (e.g. beyond KL when the student cannot match the teacher) and integrates with RL fine-tuning (RLHF). The paper reports results on summarization, translation, arithmetic reasoning, and instruction-tuning. Used in TRL via [`experimental.gkd.GKDTrainer`]. To reproduce the paper's setting, use this configuration: ```python from trl.experimental.gkd import GKDConfig # XSum summarization task (Table A.1 of the paper) training_args = GKDConfig( lmbda=0.5, # λ student data fraction (Section 3 of the paper) beta=0.5, # β Generalized JSD interpolation, 0=KL, 1=reverse KL (Section 3 of the paper) temperature=1.0, # student training temperature (Appendix A of the paper) max_steps=40000, # training steps (Table A.1 of the paper) learning_rate=3e-4, # learning rate (Table A.1 of the paper) per_device_train_batch_size=32, # batch size (Table A.1 of the paper) warmup_steps=2000, # warm-up steps (Table A.1 of the paper) max_new_tokens=64, # max output tokens (Table A.1 of the paper) ) ``` ### On-Policy Distillation **📰 Blog**: https://thinkingmachines.ai/blog/on-policy-distillation/ On-Policy Distillation involves a student model generating rollouts for each batch of training data. We subsequently obtain the probability distributions for each token of the rollouts from both the student and teacher models. The student model is then optimized to minimize the negative Kullback-Leibler (KL) divergence between its own token distributions and those of the teacher model. | Method | Sampling | Reward signal | |-------------------------|------------|---------------| | Supervised finetuning | off-policy | dense | | Reinforcement learning | on-policy | sparse | | On-policy distillation | on-policy | dense | On-Policy Distillation has been shown to outperform SFT, GRPO and can be used to restore generalization capabilities lost during SFT. Additionally on-policy distillation is more compute efficient and is less prone to overfitting when trained with limited data. To train a model with on-policy distillation using TRL, you can use the following configuration, with the [`experimental.gkd.GKDTrainer`] and [`experimental.gkd.GKDConfig`]: ```python from trl.experimental.gkd import GKDConfig training_args = GKDConfig( lmbda=1.0, # student produces rollouts for all batches beta=1.0, # to ensure reverse-kl as the loss function teacher_model_name_or_path="teacher-model", # specify the teacher model ) ``` Alternatively, you can use the [`GOLDTrainer`] and [`GOLDConfig`] to perform on-policy distillation with a similar configuration: ```python from trl.experimental import GOLDConfig config = GOLDConfig( lmbda=1.0, # student produces rollouts for all batches beta=1.0, # to ensure reverse-kl as the loss function teacher_model_name_or_path="teacher-model", # specify the teacher model ) ``` ### Knowledge Distillation of Large Language Models **📜 Paper**: https://huggingface.co/papers/2306.08543 MiniLLM is the first on-policy knowledge distillation method, which minimizes the sequence-level reverse KLD between the teacher and the student model and is optimized by reinforcement learning. It is a generalized version of [Think Machine Lab's On-Policy Distillation](https://thinkingmachines.ai/blog/on-policy-distillation/), with the option to add distribution-level single-step distillation signals (like GKD when `beta=1`) and long-context reverse KLD signals. Alternatively, you can use the [`experimental.MiniLLMTrainer`] and [`experimental.MiniLLMConfig`] to perform MiniLLM distillation as follows: ```python from datasets import load_dataset from trl.experimental.minillm import MiniLLMTrainer dataset = load_dataset("trl-lib/tldr", split="train") trainer = MiniLLMTrainer( model="Qwen/Qwen3-0.6B", teacher_model="Qwen/Qwen3-1.7B", train_dataset=dataset, ) trainer.train() ``` For more details, see the [MiniLLM Trainer documentation](minillm) documentation. ## Distributed Training ### ZeRO: Memory Optimizations Toward Training Trillion Parameter Models **📜 Paper**: https://huggingface.co/papers/1910.02054 ZeRO (Zero Redundancy Optimizer) eliminates memory redundancies in data- and model-parallel training by partitioning optimizer states, gradients, and parameters across devices while retaining low communication volume and high computational granularity. This allows for the efficient training of large models that would otherwise not fit in GPU memory. TRL supports ZeRO via the [DeepSpeed integration](deepspeed_integration). To use it, provide a DeepSpeed configuration file with your desired settings, ```yaml # config.yaml distributed_type: DEEPSPEED num_processes: 2 deepspeed_config: zero_stage: 3 ``` and launch the training script using `accelerate launch --config_file config_file`. ```sh accelerate launch --config_file config.yaml train.py ``` ## Proximal Policy Optimization Papers relating to the [`experimental.ppo.PPOTrainer`] ### Proximal Policy Optimization Algorithms **📜 Paper**: https://huggingface.co/papers/1707.06347 Introduces Proximal Policy Optimization (PPO): policy gradient methods that alternate between collecting rollouts and optimizing a clipped surrogate objective over multiple minibatch epochs. PPO retains benefits of trust-region methods (e.g. TRPO) with simpler implementation and strong empirical sample efficiency, and was validated on robotics and Atari benchmarks. Used in TRL via [`experimental.ppo.PPOTrainer`]. To use PPO with TRL, use this configuration: ```python from trl.experimental.ppo import PPOConfig training_args = PPOConfig( cliprange=0.2, # ε clipping range (Section 3 and Table 3 of the paper, Mujoco setting) num_ppo_epochs=4, # K epochs of minibatch updates (TRL default; paper uses K=10 Mujoco, K=3 Atari) gamma=1.0, # γ discount factor (TRL default for LLM tasks; paper uses γ=0.99) lam=0.95, # λ GAE parameter (Table 3 of the paper, Mujoco setting) kl_coef=0.05, # KL penalty coefficient (Section 4 of the paper discusses adaptive KL) vf_coef=0.1, # c₁ value function loss weight (Equation 9 of the paper) ) ``` ================================================ FILE: docs/source/papo_trainer.md ================================================ # PAPO Trainer [![model badge](https://img.shields.io/badge/All_models-PAPO-blue)](https://huggingface.co/models?other=papo,trl) TRL supports the Perception-Aware Policy Optimization (PAPO) as described in the paper [Perception-Aware Policy Optimization for Multimodal Reasoning](https://huggingface.co/papers/2507.06448) by [Zhenhailong Wang](https://huggingface.co/mikewang), Xuehang Guo, Sofia Stoica, [Haiyang Xu](https://huggingface.co/xhyandwyy), Hongru Wang, Hyeonjeong Ha, Xiusi Chen, Yangyi Chen, Ming Yan, Fei Huang, Heng Ji The abstract from the paper is the following: > Reinforcement Learning with Verifiable Rewards (RLVR) has proven to be a highly effective strategy for endowing Large Language Models (LLMs) with robust multi-step reasoning abilities. However, its design and optimizations remain tailored to purely textual domains, resulting in suboptimal performance when applied to multimodal reasoning tasks. In particular, we observe that a major source of error in current multimodal reasoning lies in the perception of visual inputs. To address this bottleneck, we propose Perception-Aware Policy Optimization (PAPO), a simple yet effective extension of GRPO that encourages the model to learn to perceive while learning to reason, entirely from internal supervision signals. Notably, PAPO does not rely on additional data curation, external reward models, or proprietary models. Specifically, we introduce the Implicit Perception Loss in the form of a KL divergence term to the GRPO objective, which, despite its simplicity, yields significant overall improvements (4.4%) on diverse multimodal benchmarks. The improvements are more pronounced, approaching 8.0%, on tasks with high vision dependency. We also observe a substantial reduction (30.5%) in perception errors, indicating improved perceptual capabilities with PAPO. We conduct comprehensive analysis of PAPO and identify a unique loss hacking issue, which we rigorously analyze and mitigate through a Double Entropy Loss. Overall, our work introduces a deeper integration of perception-aware supervision into RLVR learning objectives and lays the groundwork for a new RL framework that encourages visually grounded reasoning. Project page: https://mikewangwzhl.github.io/PAPO. ## PAPOTrainer [[autodoc]] experimental.papo.PAPOTrainer - train - save_model - push_to_hub ## PAPOConfig [[autodoc]] experimental.papo.PAPOConfig ================================================ FILE: docs/source/peft_integration.md ================================================ # PEFT Integration TRL supports [PEFT](https://github.com/huggingface/peft) (Parameter-Efficient Fine-Tuning) methods for memory-efficient model training. PEFT enables fine-tuning large language models by training only a small number of additional parameters while keeping the base model frozen, significantly reducing computational costs and memory requirements. This guide covers how to use PEFT with different TRL trainers, including LoRA, QLoRA, and prompt tuning techniques. For a complete working example, see the [SFT with LoRA/QLoRA notebook](https://github.com/huggingface/trl/blob/main/examples/notebooks/sft_trl_lora_qlora.ipynb). ## Installation To use PEFT with TRL, install the required dependencies: ```bash pip install trl[peft] ``` For QLoRA support (4-bit and 8-bit quantization), also install: ```bash pip install bitsandbytes ``` ## Quick Start All TRL trainers support PEFT through the `peft_config` argument. The simplest way to enable PEFT is by using the command-line interface with the `--use_peft` flag: ```bash python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-SFT-LoRA ``` Alternatively, you can pass a PEFT config directly in your Python code: ```python from peft import LoraConfig from trl import SFTTrainer # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Configure training - note the higher learning rate for LoRA (10x base rate) training_args = SFTConfig( learning_rate=2.0e-4, # 10x the base rate (2.0e-5) for LoRA ... ) # Create trainer with PEFT trainer = SFTTrainer( model=model, train_dataset=dataset, peft_config=peft_config, ) ``` ## Three Ways to Configure PEFT TRL provides three different methods to configure PEFT, each suited for different use cases: ### 1. Using CLI Flags (Simplest) The easiest way to enable PEFT is to use the `--use_peft` flag with the command-line interface. This method is ideal for quick experiments and standard configurations: ```bash python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --lora_dropout 0.05 \ --output_dir Qwen2-0.5B-SFT-LoRA ``` **Pros**: Quick setup, no code required **Cons**: Limited to LoRA, fewer customization options ### 2. Passing peft_config to Trainer (Recommended) For more control, pass a PEFT configuration directly to the trainer. This is the recommended approach for most use cases: ```python from peft import LoraConfig from trl import SFTConfig, SFTTrainer peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], ) trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset, peft_config=peft_config, # Pass config here ) ``` **Pros**: Full control, supports all PEFT methods (LoRA, Prompt Tuning, etc.) **Cons**: Requires Python code ### 3. Applying PEFT to Model Directly (Advanced) For maximum flexibility, you can apply PEFT to your model before passing it to the trainer: ```python from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM from trl import SFTConfig, SFTTrainer # Load base model model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B") # Apply PEFT configuration peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, peft_config) # Pass PEFT-wrapped model to trainer trainer = SFTTrainer( model=model, # Already has PEFT applied args=training_args, train_dataset=dataset, # Note: no peft_config needed here ) ``` **Pros**: Maximum control, useful for custom model architectures or complex setups **Cons**: More verbose, requires understanding of PEFT internals ## Learning Rate Considerations When using LoRA or other PEFT methods, you typically need to use a **higher learning rate** (approximately 10x) compared to full fine-tuning. This is because PEFT methods train only a small fraction of parameters, requiring a larger learning rate to achieve similar parameter updates. **Recommended learning rates:** | Trainer | Full Fine-Tuning | With LoRA (10x) | |---------|------------------|-----------------| | **SFT** | `2.0e-5` | `2.0e-4` | | **DPO** | `5.0e-7` | `5.0e-6` | | **GRPO** | `1.0e-6` | `1.0e-5` | | **Prompt Tuning** | N/A | `1.0e-2` to `3.0e-2` | > **Why 10x?** LoRA adapters have significantly fewer trainable parameters than the full model. A higher learning rate compensates for this reduced parameter count, ensuring effective training. For detailed explanation, see [this blog post](https://thinkingmachines.ai/blog/lora/). For additional best practices on using LoRA effectively, refer to the [LoRA Without Regret](lora_without_regret) documentation. ## PEFT with Different Trainers TRL's trainers support PEFT configurations for various training paradigms. Below are detailed examples for each major trainer. ### Supervised Fine-Tuning (SFT) The `SFTTrainer` is used for supervised fine-tuning on instruction datasets. #### With LoRA ```bash python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2.0e-4 \ --num_train_epochs 1 \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-SFT-LoRA ``` #### Python Example ```python from peft import LoraConfig from trl import SFTConfig, SFTTrainer # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", target_modules=["q_proj", "v_proj"], # optional: specify target modules ) # Configure training with higher learning rate for LoRA training_args = SFTConfig( learning_rate=2.0e-4, # 10x the base rate for LoRA ... ) # Create trainer with PEFT config trainer = SFTTrainer( model="Qwen/Qwen2-0.5B", # can pass model name or loaded model args=training_args, train_dataset=dataset, peft_config=peft_config, # pass PEFT config here ) trainer.train() ``` ### Direct Preference Optimization (DPO) The [`DPOTrainer`] implements preference learning from human feedback. #### With LoRA ```bash python trl/scripts/dpo.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --learning_rate 5.0e-6 \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-DPO-LoRA ``` #### Python Example ```python from peft import LoraConfig from trl import DPOConfig, DPOTrainer # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Configure training with higher learning rate for LoRA training_args = DPOConfig( learning_rate=5.0e-6, # 10x the base rate for DPO with LoRA ... ) # Create trainer with PEFT config trainer = DPOTrainer( model="Qwen/Qwen2-0.5B", # can pass model name or loaded model args=training_args, train_dataset=dataset, peft_config=peft_config, # pass PEFT config here ) trainer.train() ``` **Note:** When using PEFT with DPO, you don't need to provide a separate reference model (`ref_model`). The trainer automatically uses the frozen base model as the reference. ### Group Relative Policy Optimization (GRPO) The `GRPOTrainer` optimizes policies using group-based rewards. #### With LoRA ```bash python trl/scripts/grpo.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/math-reasoning \ --learning_rate 1.0e-5 \ --per_device_train_batch_size 2 \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-GRPO-LoRA ``` #### Python Example ```python from peft import LoraConfig from trl import GRPOConfig, GRPOTrainer # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Configure training with higher learning rate for LoRA training_args = GRPOConfig( learning_rate=1.0e-5, # 10x the base rate for GRPO with LoRA ... ) # Create trainer with PEFT config trainer = GRPOTrainer( model="Qwen/Qwen2-0.5B", # can pass model name or loaded model args=training_args, train_dataset=dataset, peft_config=peft_config, # pass PEFT config here ) trainer.train() ``` ### Proximal Policy Optimization (PPO) #### Multi-Adapter RL Training You can use a single base model with multiple PEFT adapters for the entire PPO algorithm - including retrieving reference logits, computing active logits, and calculating rewards. This approach is useful for memory-efficient RL training. > [!WARNING] > This feature is experimental and convergence has not been extensively tested. We encourage the community to share feedback and report any issues. **Requirements** Install PEFT and optionally bitsandbytes for 8-bit models: ```bash pip install peft bitsandbytes ``` **Training Workflow** The multi-adapter approach requires three stages: 1. **Supervised Fine-Tuning (SFT)**: Train a base model on your target domain (e.g., IMDB dataset) using `SFTTrainer` 2. **Reward Model Training**: Train a reward model adapter using PEFT and `RewardTrainer` (see [reward modeling example](https://github.com/huggingface/trl/tree/main/examples/scripts/reward_modeling.py)) 3. **PPO Training**: Fine-tune new adapters using PPO with the reward adapter > [!IMPORTANT] > Use the same base model (architecture and weights) for stages 2 & 3. **Basic Usage** After training your reward adapter and pushing it to the Hub: ```python from peft import LoraConfig from trl.experimental.ppo import PPOTrainer, AutoModelForCausalLMWithValueHead model_name = "huggyllama/llama-7b" rm_adapter_id = "trl-lib/llama-7b-hh-rm-adapter" # Configure PPO adapter lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Load model with reward adapter model = AutoModelForCausalLMWithValueHead.from_pretrained( model_name, peft_config=lora_config, reward_adapter=rm_adapter_id, ) trainer = PPOTrainer(model=model, ...) ``` In your training loop, compute rewards using: ```python rewards = trainer.model.compute_reward_score(**inputs) ``` **Advanced Features** **Quantized Base Models** For memory-efficient training, load the base model in 8-bit or 4-bit while keeping adapters in float32: ```python from transformers import BitsAndBytesConfig model = AutoModelForCausalLMWithValueHead.from_pretrained( model_name, peft_config=lora_config, reward_adapter=rm_adapter_id, quantization_config=BitsAndBytesConfig(load_in_8bit=True), ) ``` ## QLoRA: Quantized Low-Rank Adaptation QLoRA combines 4-bit quantization with LoRA to enable fine-tuning of very large models on consumer hardware. This technique can reduce memory requirements by up to 4x compared to standard LoRA. ### How QLoRA Works 1. **4-bit Quantization**: The base model is loaded in 4-bit precision using `bitsandbytes` 2. **Frozen Weights**: The quantized model weights remain frozen during training 3. **LoRA Adapters**: Only the LoRA adapter parameters are trained in higher precision 4. **Memory Efficiency**: Enables fine-tuning of models like Llama-70B on a single consumer GPU ### Using QLoRA with TRL Simply combine `load_in_4bit=True` with PEFT configuration: #### Command Line ```bash python trl/scripts/sft.py \ --model_name_or_path meta-llama/Llama-2-7b-hf \ --dataset_name trl-lib/Capybara \ --load_in_4bit \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 16 \ --output_dir Llama-2-7b-QLoRA ``` #### Python Example ```python import torch from peft import LoraConfig from transformers import AutoModelForCausalLM, BitsAndBytesConfig from trl import SFTConfig, SFTTrainer # Configure 4-bit quantization bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) # Load model with quantization model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-7b-hf", quantization_config=bnb_config, device_map="auto", ) # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Configure training with higher learning rate for LoRA training_args = SFTConfig( learning_rate=2.0e-4, # 10x the base rate for QLoRA ... ) # Create trainer with PEFT config trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset, peft_config=peft_config, ) trainer.train() ``` ### QLoRA Configuration Options The `BitsAndBytesConfig` provides several options to optimize memory and performance: ```python import torch from transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", # or "fp4" bnb_4bit_compute_dtype=torch.bfloat16, # Compute dtype for 4-bit base models bnb_4bit_use_double_quant=True, # Nested quantization for additional memory savings ) ``` **Configuration Parameters:** - `bnb_4bit_quant_type`: Quantization data type (`"nf4"` or `"fp4"`). NF4 is recommended. - `bnb_4bit_compute_dtype`: The dtype used for computation. Use `bfloat16` for better training stability. - `bnb_4bit_use_double_quant`: Enable nested quantization to save additional ~0.4 bits per parameter. ### 8-bit Quantization For slightly higher precision with reduced memory savings, you can use 8-bit quantization: ```python from transformers import BitsAndBytesConfig, AutoModelForCausalLM bnb_config = BitsAndBytesConfig(load_in_8bit=True) model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-7b-hf", quantization_config=bnb_config, device_map="auto", ) ``` Or via command line: ```bash python trl/scripts/sft.py \ --model_name_or_path meta-llama/Llama-2-7b-hf \ --load_in_8bit \ --use_peft \ --lora_r 32 \ --lora_alpha 16 ``` ## Prompt Tuning Prompt tuning is another PEFT technique that learns soft prompts (continuous embeddings) prepended to the input, while keeping the entire model frozen. This is particularly effective for large models. ### How Prompt Tuning Works 1. **Virtual Tokens**: Adds learnable continuous embeddings (virtual tokens) to the input 2. **Frozen Model**: The entire base model remains frozen 3. **Task-Specific Prompts**: Each task learns its own prompt embeddings 4. **Extreme Efficiency**: Only the prompt embeddings are trained (typically 8-20 tokens) ### Using Prompt Tuning with TRL ```python from peft import PromptTuningConfig, PromptTuningInit, TaskType from trl import SFTConfig, SFTTrainer # Configure Prompt Tuning peft_config = PromptTuningConfig( task_type=TaskType.CAUSAL_LM, prompt_tuning_init=PromptTuningInit.TEXT, num_virtual_tokens=8, prompt_tuning_init_text="Classify if the tweet is a complaint or not:", tokenizer_name_or_path="Qwen/Qwen2-0.5B", ) # Configure training with higher learning rate for Prompt Tuning training_args = SFTConfig( learning_rate=2.0e-2, # Prompt Tuning typically uses 1e-2 to 3e-2 ... ) # Create trainer with PEFT config trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset, peft_config=peft_config, # pass PEFT config here ) trainer.train() ``` ### Prompt Tuning Configuration ```python from peft import PromptTuningConfig, PromptTuningInit, TaskType peft_config = PromptTuningConfig( task_type=TaskType.CAUSAL_LM, # Task type prompt_tuning_init=PromptTuningInit.TEXT, # Initialize from text num_virtual_tokens=8, # Number of virtual tokens prompt_tuning_init_text="Your initialization text here", tokenizer_name_or_path="model_name", ) ``` **Configuration Parameters:** - `task_type`: The task type (`TaskType.CAUSAL_LM` for language modeling) - `prompt_tuning_init`: Initialization method (`TEXT`, `RANDOM`) - `num_virtual_tokens`: Number of virtual tokens to prepend (typically 8-20) - `prompt_tuning_init_text`: Text to initialize the virtual tokens (when using `TEXT` init) - `tokenizer_name_or_path`: Tokenizer for initializing from text ### Prompt Tuning vs LoRA | Feature | Prompt Tuning | LoRA | |---------|---------------|------| | **Parameters Trained** | ~0.001% | ~0.1-1% | | **Memory Usage** | Minimal | Low | | **Training Speed** | Fastest | Fast | | **Model Modification** | None | Adapter layers | | **Best For** | Large models, many tasks | General fine-tuning | | **Learning Rate** | Higher (1e-2 to 3e-2) | Standard (1e-4 to 3e-4) | ## Advanced PEFT Configurations ### LoRA Configuration Parameters ```python from peft import LoraConfig peft_config = LoraConfig( r=16, # LoRA rank lora_alpha=32, # LoRA scaling factor lora_dropout=0.05, # Dropout probability bias="none", # Bias training strategy task_type="CAUSAL_LM", # Task type target_modules=["q_proj", "v_proj"], # Modules to apply LoRA modules_to_save=None, # Additional modules to train ) ``` **Key Parameters:** - `r`: LoRA rank (typical values: 8, 16, 32, 64). Higher rank = more parameters but potentially better performance. - `lora_alpha`: Scaling factor (typically 2x the rank). Controls the magnitude of LoRA updates. - `lora_dropout`: Dropout probability for LoRA layers (typical: 0.05-0.1). - `target_modules`: Which modules to apply LoRA to. Common choices: - `["q_proj", "v_proj"]`: Attention query and value (memory efficient) - `["q_proj", "k_proj", "v_proj", "o_proj"]`: All attention projections - `["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]`: All linear layers - `modules_to_save`: Additional modules to fully train (e.g., `["embed_tokens", "lm_head"]`) ### Target Module Selection You can specify which modules to apply LoRA to. Common patterns: ```python # Minimal (most memory efficient) target_modules=["q_proj", "v_proj"] # Attention only target_modules=["q_proj", "k_proj", "v_proj", "o_proj"] # All linear layers (best performance, more memory) target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] ``` ### Using Command-Line Arguments TRL scripts accept PEFT parameters via command line: ```bash python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --lora_dropout 0.05 \ --lora_target_modules q_proj v_proj \ --output_dir output ``` Available flags: - `--use_peft`: Enable PEFT - `--lora_r`: LoRA rank (default: 16) - `--lora_alpha`: LoRA alpha (default: 32) - `--lora_dropout`: LoRA dropout (default: 0.05) - `--lora_target_modules`: Target modules (space-separated) - `--lora_modules_to_save`: Additional modules to train - `--use_rslora`: Enable Rank-Stabilized LoRA - `--use_dora`: Enable Weight-Decomposed LoRA (DoRA) - `--load_in_4bit`: Enable 4-bit quantization (QLoRA) - `--load_in_8bit`: Enable 8-bit quantization ## Saving and Loading PEFT Models ### Saving After training, save your PEFT adapters: ```python # Save the adapters trainer.save_model("path/to/adapters") # Or manually model.save_pretrained("path/to/adapters") ``` This saves only the adapter weights (~few MB) rather than the full model (~several GB). ### Loading Load a PEFT model for inference: ```python from transformers import AutoModelForCausalLM from peft import PeftModel # Load base model base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B") # Load PEFT adapters model = PeftModel.from_pretrained(base_model, "path/to/adapters") # Optionally merge adapters into base model for faster inference model = model.merge_and_unload() ``` ### Pushing to Hub You can easily share your PEFT adapters on the Hugging Face Hub: ```python # Push adapters to Hub model.push_to_hub("username/model-name-lora") # Load from Hub from peft import PeftModel model = PeftModel.from_pretrained(base_model, "username/model-name-lora") ``` ## Multi-GPU Training PEFT works seamlessly with TRL's multi-GPU support through `accelerate`: ```bash # Configure accelerate accelerate config # Launch training accelerate launch trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --use_peft \ --lora_r 32 \ --lora_alpha 16 ``` For QLoRA with multiple GPUs, the base model is automatically sharded: ```bash accelerate launch trl/scripts/sft.py \ --model_name_or_path meta-llama/Llama-2-70b-hf \ --load_in_4bit \ --use_peft \ --lora_r 32 ``` ### Naive Pipeline Parallelism (NPP) for Large Models For very large models (>60B parameters), TRL supports Naive Pipeline Parallelism (NPP), which distributes the model and adapters across multiple GPUs. The activations and gradients are communicated across GPUs, supporting both `int8` and other data types. ![NPP](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl-npp.png) **How to Use NPP** Load your model with a custom `device_map` to split it across multiple devices: ```python from transformers import AutoModelForCausalLM from peft import LoraConfig # Create custom device map (see accelerate documentation) device_map = { "model.embed_tokens": 0, "model.layers.0": 0, # ... distribute layers across GPUs "lm_head": 0, # Must be on GPU 0 } model = AutoModelForCausalLM.from_pretrained( "meta-llama/Llama-2-70b-hf", device_map=device_map, peft_config=lora_config, ) ``` > [!IMPORTANT] > - Keep the `lm_head` module on the first GPU (device 0) to avoid errors > - See this [tutorial on device maps](https://github.com/huggingface/blog/blob/main/accelerate-large-models.md) for proper configuration > - Run training scripts directly (not with `accelerate launch`): `python script.py` > - Data Parallelism is not yet supported with NPP ## Resources ### TRL Examples and Notebooks - **[SFT with LoRA/QLoRA Notebook](https://github.com/huggingface/trl/blob/main/examples/notebooks/sft_trl_lora_qlora.ipynb)** - Complete working example showing both LoRA and QLoRA implementations - **[TRL Examples Directory](https://github.com/huggingface/trl/tree/main/examples)** - Collection of training scripts demonstrating PEFT with different trainers - **[TRL Cookbook Recipes](https://github.com/huggingface/cookbook/tree/main/notebooks/transformers)** - Step-by-step guides for common PEFT training scenarios ### Documentation - [PEFT Documentation](https://huggingface.co/docs/peft) - Official PEFT library documentation - [TRL Documentation](https://huggingface.co/docs/trl) - Complete TRL documentation with trainer guides - [LoRA Without Regret](lora_without_regret) - Best practices for using LoRA effectively ### Research Papers - [LoRA Paper](https://huggingface.co/papers/2106.09685) - Original LoRA methodology and results - [QLoRA Paper](https://huggingface.co/papers/2305.14314) - Efficient finetuning with 4-bit quantization - [Prompt Tuning Paper](https://huggingface.co/papers/2104.08691) - The Power of Scale for Parameter-Efficient Prompt Tuning ================================================ FILE: docs/source/ppo_trainer.md ================================================ # PPO Trainer [![model badge](https://img.shields.io/badge/All_models-PPO-blue)](https://huggingface.co/models?other=ppo,trl) TRL supports training LLMs with [Proximal Policy Optimization (PPO)](https://huggingface.co/papers/1707.06347). References: - [Fine-Tuning Language Models from Human Preferences](https://github.com/openai/lm-human-preferences) - [Learning to Summarize from Human Feedback](https://github.com/openai/summarize-from-feedback) - [The N Implementation Details of RLHF with PPO](https://huggingface.co/blog/the_n_implementation_details_of_rlhf_with_ppo) - [The N+ Implementation Details of RLHF with PPO: A Case Study on TL;DR Summarization](https://huggingface.co/papers/2403.17031) ## Get started To just run a PPO script to make sure the trainer can run, you can run the following command to train a PPO model with a dummy reward model. ```bash python examples/scripts/ppo/ppo.py \ --dataset_name trl-internal-testing/descriptiveness-sentiment-trl-style \ --dataset_train_split descriptiveness \ --learning_rate 3e-6 \ --num_ppo_epochs 1 \ --num_mini_batches 1 \ --output_dir models/minimal/ppo \ --per_device_train_batch_size 64 \ --gradient_accumulation_steps 1 \ --total_episodes 10000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --sft_model_path EleutherAI/pythia-1b-deduped \ --reward_model_path EleutherAI/pythia-1b-deduped \ --missing_eos_penalty 1.0 ``` ## Explanation of the logged metrics The logged metrics are as follows. Here is an example [tracked run at Weights and Biases](https://wandb.ai/huggingface/trl/runs/dd2o3g35) - `eps`: Tracks the number of episodes per second. - `objective/kl`: The mean Kullback-Leibler (KL) divergence between the current policy and reference policy. - `objective/entropy`: The mean entropy of the policy, indicating the randomness of the actions chosen by the policy. - `objective/non_score_reward`: The mean reward from non-score-related sources, basically `beta * kl.sum(1)`, where `beta` is the KL penalty coefficient and `kl` is the per-token KL divergence. - `objective/rlhf_reward`: The mean RLHF reward, which is `score - non_score_reward`. - `objective/scores`: The mean scores returned by the reward model / environment. - `policy/approxkl_avg`: The average approximate KL divergence between consecutive PPO policies. Note that this is not the same as `objective/kl`. - `policy/clipfrac_avg`: The average fraction of policy updates that are clipped, indicating how often the policy updates are constrained to prevent large changes. - `loss/policy_avg`: The average policy loss, indicating how well the policy is performing. - `loss/value_avg`: The average value loss, indicating the difference between the predicted value and the actual reward. - `val/clipfrac_avg`: The average fraction of value function updates that are clipped, similar to policy/clipfrac_avg but for the value function. - `policy/entropy_avg`: The average entropy of the policy during training, indicating how diverse the policy's actions are. - `val/ratio`: The mean ratio of the current policy probability to the old policy probability, providing a measure of how much the policy has changed. - `val/ratio_var`: The variance of the `val/ratio`, indicating the variability in policy changes. - `val/num_eos_tokens`: The number of end-of-sequence (EOS) tokens generated, which can indicate the number of complete responses. - `lr`: lr: The current learning rate used by the optimizer. - `episode`: episode: The current episode count in the training process. ## Cookbook - Debugging TIP: `objective/rlhf_reward`: this is the ultimate objective of the RLHF training. If training works as intended, this metric should keep going up. - Debugging TIP: `val/ratio`: this number should float around 1.0, and it gets clipped by `--cliprange 0.2` with PPO's surrogate loss. So if this `ratio` is too high like 2.0 or 1000.0 or too small like 0.1, it means the updates between consecutive policies are too drastic. You should try understand why this is happening and try to fix it. - Memory TIP: If you are running out of memory, you can try to reduce the `--per_device_train_batch_size` or increase the `--gradient_accumulation_steps` to reduce the memory footprint. - Memory TIP: If you have multiple GPUs, you can also run training with DeepSpeed stage 3 to reduce the memory footprint `accelerate launch --config_file examples/accelerate_configs/deepspeed_zero3.yaml`. - Usage TIP: We recommend to use the "EOS trick" via `--missing_eos_penalty`, which subtracts a static scalar penalty from the score of completions that do not end with an EOS token. This can help the model learn to generate more coherent completions. ## What is my model doing exactly? To help you understand what your model is doing, we periodically log some sample completions from the model. Here is an example of a completion. In an example [tracked run at Weights and Biases](https://wandb.ai/huggingface/trl/runs/dd2o3g35), it looks like the following, allowing you to see the model's response at different stages of training. By default we generate `--num_sample_generations 10` during training, but you can customize the number of generations. ![ppov2_completions](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/ppov2_completions.gif) In the logs the sampled generations look like ```txt ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓ ┃ query ┃ model response ┃ score ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩ │ SUBREDDIT: r/AskReddit │ I'm in love with a friend, and │ 3.921875 │ │ │ I don't know how to get rid of │ │ │ TITLE: How do you get someone │ those feelings. I'm │ │ │ out of your head? │ desperate.<|endoftext|>[PAD][P… │ │ │ │ │ │ │ POST: Hi, │ │ │ │ I'm 22, and I have been with my │ │ │ │ girlfriend for 5 years now. We │ │ │ │ recently moved together. We've │ │ │ │ always loved each other │ │ │ │ intensely. │ │ │ │ │ │ │ │ Problem, I recently started to │ │ │ │ have feelings for an other │ │ │ │ person (a friend). This person │ │ │ │ has had a boyfriend for now 3 │ │ │ │ years, and has absolutely no │ │ │ │ ideas. Those feelings were so │ │ │ │ strong, it was hard to hide │ │ │ │ them. After 2 months of me │ │ │ │ being distant and really sad, │ │ │ │ my girlfriend forced me to say │ │ │ │ what was bothering me. I'm not │ │ │ │ a good liar, and now she knows. │ │ │ │ │ │ │ │ We decided to give us a week │ │ │ │ alone, I went to my parents. │ │ │ │ │ │ │ │ Now, I'm completely lost. I │ │ │ │ keep on thinking about this │ │ │ │ person, and I hate that. I │ │ │ │ would like for those feelings │ │ │ │ to go away, to leave me alone. │ │ │ │ But I can't. │ │ │ │ │ │ │ │ What do I do? It's been 3 │ │ │ │ months now, and I'm just │ │ │ │ desperate. │ │ │ │ │ │ │ │ TL;DR: │ │ │ ├─────────────────────────────────┼─────────────────────────────────┼──────────┤ │ SUBREDDIT: r/pettyrevenge │ My mom woke me up with a loud │ 6.84375 │ │ │ TV. I blasted Gangnam Style on │ │ │ TITLE: So, my mom woke me up │ repeat, with the bass cranked │ │ │ with a loud TV. │ up as high as it could │ │ │ │ go.<|endoftext|>[PAD][PAD][PAD… │ │ │ POST: She was in her living │ │ │ │ room, watching TV. This was at │ │ │ │ about 8:30 in the morning, and │ │ │ │ she was exercising. She turned │ │ │ │ the TV up extra loud to hear it │ │ │ │ over her excercycle, and woke │ │ │ │ me up. I went in there asking │ │ │ │ for her to turn it down. She │ │ │ │ said she didn't have to; I │ │ │ │ explained that I always used │ │ │ │ headphones so she didn't have │ │ │ │ to deal with my noise and that │ │ │ │ she should give me a little │ │ │ │ more respect, given that I paid │ │ │ │ rent at the time. │ │ │ │ │ │ │ │ She disagreed. I went back to │ │ │ │ my room, rather pissed off at │ │ │ │ the lack of equality. I had no │ │ │ │ lock on my door; but I had a │ │ │ │ dresser right next to it, so I │ │ │ │ pulled one of the drawers out │ │ │ │ enough so that it caused the │ │ │ │ door to not be openable. Then, │ │ │ │ I turned my speakers up really │ │ │ │ loud and blasted Gangnam Style │ │ │ │ on repeat, with the bass │ │ │ │ cranked up as high as it could │ │ │ │ go. │ │ │ │ │ │ │ │ If you hate Gangnam Style for │ │ │ │ being overplayed, you will see │ │ │ │ why I chose that particular │ │ │ │ song. I personally don't mind │ │ │ │ it. But here's the thing about │ │ │ │ my bass; it vibrates the walls, │ │ │ │ making one hell of a lot of │ │ │ │ noise. Needless to say, my mom │ │ │ │ was not pleased and shut off │ │ │ │ the internet. But it was oh so │ │ │ │ worth it. │ │ │ │ │ │ │ │ TL;DR: │ │ │ └─────────────────────────────────┴─────────────────────────────────┴──────────┘ ``` ## Implementation details This PPO implementation is based on the [The N+ Implementation Details of RLHF with PPO: A Case Study on TL;DR Summarization](https://huggingface.co/papers/2403.17031). ## Benchmark experiments To validate the PPO implementation works, we ran experiment on the 1B model. Here are the command we used to run the experiment. We take the SFT / RM models directly from [The N+ Implementation Details of RLHF with PPO: A Case Study on TL;DR Summarization](https://huggingface.co/papers/2403.17031). ```shell accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/ppo/ppo_tldr.py \ --dataset_name trl-lib/tldr \ --dataset_test_split validation \ --output_dir models/minimal/ppo_tldr \ --learning_rate 3e-6 \ --per_device_train_batch_size 16 \ --gradient_accumulation_steps 4 \ --total_episodes 1000000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --sft_model_path cleanrl/EleutherAI_pythia-1b-deduped__sft__tldr \ --reward_model_path cleanrl/EleutherAI_pythia-1b-deduped__reward__tldr \ --local_rollout_forward_batch_size 16 \ --missing_eos_penalty 1.0 \ --stop_token eos \ --eval_strategy steps \ --eval_steps 100 ``` Checkpoints and experiment tracking are available at: - [🤗 Model checkpoint](https://huggingface.co/trl-lib/ppo_tldr) - [🐝 Tracked experiment](https://wandb.ai/huggingface/trl/runs/dd2o3g35) To evaluate, we use [vLLM](https://github.com/vllm-project/vllm) to load the checkpoints and GPT-4o mini as a judge model to evaluate the generated TL;DR against the reference TL;DR. For more information on how to use judges, see [Judges](judges). ```bash $ python examples/scripts/evals/judge_tldr.py --model_name_or_path cleanrl/EleutherAI_pythia-1b-deduped__sft__tldr --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 33.00% $ python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/ppo_tldr --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 64.70% ``` The PPO checkpoint gets a 64.7% preferred rate vs the 33.0% preference rate of the SFT checkpoint. This is a good sign that the PPO training is working as intended. Metrics: ![PPO v2](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/ppov2.png) ```bash # pip install openrlbenchmark==0.2.1a5 # see https://github.com/openrlbenchmark/openrlbenchmark#get-started for documentation # to use it, change `?we=huggingface&wpn=trl` to your own project and `?tag=pr-1540` to your own tag python -m openrlbenchmark.rlops_multi_metrics \ --filters '?we=huggingface&wpn=trl&xaxis=train/episode&ceik=output_dir&cen=sft_model_path&metrics=train/objective/rlhf_reward&metrics=train/objective/scores&metrics=train/objective/kl&metrics=train/objective/non_score_reward&metrics=train/objective/entropy&metrics=train/policy/approxkl_avg&metrics=train/policy/clipfrac_avg&metrics=train/loss/policy_avg&metrics=train/loss/value_avg&metrics=train/val/clipfrac_avg&metrics=train/policy/entropy_avg&metrics=train/val/ratio&metrics=train/val/ratio_var&metrics=train/val/num_eos_tokens&metrics=train/lr&metrics=train/eps' \ "cleanrl/EleutherAI_pythia-1b-deduped__sft__tldr?tag=pr-1540" \ --env-ids models/minimal/ppo_tldr \ --pc.ncols 4 \ --pc.ncols-legend 1 \ --pc.xlabel "Episode" \ --output-filename benchmark/trl/pr-1540/ppo \ --scan-history ``` ## PPOTrainer [[autodoc]] experimental.ppo.PPOTrainer - train - save_model - push_to_hub ## PPOConfig [[autodoc]] experimental.ppo.PPOConfig ## PreTrainedModelWrapper [[autodoc]] experimental.ppo.PreTrainedModelWrapper ## AutoModelForCausalLMWithValueHead [[autodoc]] experimental.ppo.AutoModelForCausalLMWithValueHead - __init__ - forward - generate - _init_weights ## AutoModelForSeq2SeqLMWithValueHead [[autodoc]] experimental.ppo.AutoModelForSeq2SeqLMWithValueHead - __init__ - forward - generate - _init_weights ================================================ FILE: docs/source/prm_trainer.md ================================================ # PRM Trainer [![model badge](https://img.shields.io/badge/All_models-PRM-blue)](https://huggingface.co/models?other=prm,trl) > [!WARNING] > PRM Trainer is an experimental API which is subject to change at any time. ## Overview Process-supervised Reward Models (PRM) were proposed in [Solving math word problems with process- and outcome-based feedback](https://huggingface.co/papers/2211.14275) by Jonathan Uesato, Nate Kushman, Ramana Kumar, Francis Song, Noah Siegel, Lisa Wang, Antonia Creswell, Geoffrey Irving, and Irina Higgins. The abstract from the paper is the following: > Recent work has shown that asking language models to generate reasoning steps improves performance on many reasoning tasks. When moving beyond prompting, this raises the question of how we should supervise such models: outcome-based approaches which supervise the final result, or process-based approaches which supervise the reasoning process itself? Differences between these approaches might naturally be expected not just in final-answer errors but also in reasoning errors, which can be difficult to detect and are problematic in many real-world domains such as education. We run the first comprehensive comparison between process- and outcome-based approaches trained on a natural language task, GSM8K. We find that pure outcome-based supervision produces similar final-answer error rates with less label supervision. However, for correct reasoning steps we find it necessary to use processbased supervision or supervision from learned reward models that emulate process-based feedback. In total, we improve the previous best results from 16.8% → 12.7% final-answer error and 14.0% → 3.4% reasoning error among final-answer-correct solutions. This post-training method was contributed by [Gaetan Lopez](https://github.com/gaetanlop), [Lewis Tunstall](https://huggingface.co/lewtun), [Quentin Gallouédec](https://huggingface.co/qgallouedec) and [Agustín Piqueres](https://huggingface.co/plaguss). ## Quick start This example demonstrates how to train a model using the PRM method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B) as the base model. We use the stepwise supervision data from the [Math Shepherd dataset](https://huggingface.co/datasets/trl-lib/math_shepherd). You can view the data in the dataset here: Below is the script to train the model: ```python # train_prm.py from datasets import load_dataset from trl.experimental.prm import PRMConfig, PRMTrainer from transformers import AutoModelForTokenClassification, AutoTokenizer model = AutoModelForTokenClassification.from_pretrained("Qwen/Qwen2-0.5B", num_labels=2) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B") train_dataset = load_dataset("trl-lib/math_shepherd", split="train[:10%]") training_args = PRMConfig(output_dir="Qwen2-0.5B-Reward-Math-Sheperd") trainer = PRMTrainer(model=model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_prm.py ``` Distributed across 8 GPUs, the training takes approximately 1 hour. To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-Reward-Math-Sheperd) performs, you can use the following script. ```python from datasets import load_dataset from transformers import pipeline pipe = pipeline("token-classification", model="trl-lib/Qwen2-0.5B-Reward-Math-Sheperd") dataset = load_dataset("trl-lib/math_shepherd") example = { "prompt": "Musa is the class teacher of a class of 45 students. He wants to split them into three groups by age. If a third of the class is under 11 years, and two-fifths are above 11 but under 13, how many students will be in the third group (13 years and above)?", "completions": [ "Step 1: A third of the class is under 11 years because 11 - 1/3 = <<11-1/3=7>>7.", "Step 2: Two-fifths of the class are above 11 but under 13 because 2/5 * 11 = <<2/5*11=8>>8.", "Step 3: There are 45 students, so the third group will have 45 - 7 - 8 = <<45-7-8=20>>20 students. The answer is: 20", ], "labels": [True, False, False], } separator = "\n" # It's important to use the same separator as the one used during training for idx in range(1, len(example["completions"]) + 1): steps = example["completions"][0:idx] text = separator.join((example["prompt"], *steps)) + separator # Add a separator between the prompt and each steps pred_entity = pipe(text)[-1]["entity"] pred = {"LABEL_0": False, "LABEL_1": True}[pred_entity] label = example["labels"][idx - 1] print(f"Step {idx}\tPredicted: {pred} \tLabel: {label}") ``` ```text Step 1 Predicted: True Label: True Step 2 Predicted: False Label: False Step 3 Predicted: False Label: False ``` It's a win! ## Expected dataset type PRM requires a [stepwise supervision](dataset_formats#stepwise-supervision). The dataset should contain the following columns: `prompt`, `completions` and `labels`, where `completions` contains a list of reasoning steps and `labels` a list of booleans or floats indicating the correctness of each step. The [`experimental.prm.PRMTrainer`] only supports [standard](dataset_formats#standard) dataset format. ## Example script We provide an example script to train a model using the PRM method. The script is available in [`examples/scripts/prm.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/prm.py) To use the PRM script with the [Qwen2 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B) on the [Math Shepherd dataset](https://huggingface.co/datasets/trl-lib/math_shepherd), run the following command: ```bash accelerate launch examples/scripts/prm.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/math_shepherd \ --num_train_epochs 1 \ --output_dir Qwen2-0.5B-Reward-Math-Sheperd ``` ## PRMTrainer [[autodoc]] experimental.prm.PRMTrainer - train - save_model - push_to_hub ## PRMConfig [[autodoc]] experimental.prm.PRMConfig ================================================ FILE: docs/source/ptt_integration.md ================================================ # Post-Training Toolkit Integration [Post-Training Toolkit](https://github.com/microsoft/post-training-toolkit) is a diagnostic and observability layer for RLHF training runs. Add one callback to any TRL trainer and get **auto-metrics**, **crash postmortems**, and **literature-backed heuristics**—without writing glue code. It was built to operationalize the debugging patterns we found most useful when running post-training at scale. ## Usage 1. First, install Post-Training Toolkit: ```bash pip install post-training-toolkit ``` 2. Add one callback to your trainer. That's it! ```python from post_training_toolkit import DiagnosticsCallback from trl import DPOTrainer trainer = DPOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl.experimental.ppo import PPOTrainer trainer = PPOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl import SFTTrainer trainer = SFTTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl.experimental.orpo import ORPOTrainer trainer = ORPOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl import KTOTrainer trainer = KTOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl.experimental.cpo import CPOTrainer trainer = CPOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ```python from post_training_toolkit import DiagnosticsCallback from trl import GRPOTrainer trainer = GRPOTrainer( model=model, args=training_args, callbacks=[DiagnosticsCallback()], # ← Just add this ... ) trainer.train() ``` ## What You Get **Example output:** ```text [HIGH] DPO loss stuck at ~0.693 (random chance). Model may not be learning preferences. Ref: Rafailov et al. (2023) 'DPO', Section 4.2 [RECOMMENDED] Increase learning rate 2-5x, check data quality, or reduce beta. ``` ## Example Demo See a full working example with auto-stop in action: 📂 **[demo/live_demo.ipynb](https://github.com/microsoft/post-training-toolkit/blob/main/demo/notebooks/demo_live_output.ipynb)** 📂 **[demo/scripts/custom_heuristic.py](https://github.com/microsoft/post-training-toolkit/blob/main/demo/scripts/custom_heuristic_demo.py)** ### 1. Auto-Metrics The callback automatically captures algorithm-specific metrics, backed by the latest research and industry push: | Trainer | Key Metrics Captured | |---------|---------------------| | **DPO** | loss, win_rate, reward_margin, logps_chosen/rejected | | **PPO** | policy_loss, value_loss, entropy, clip_fraction, KL | | **GRPO** | group rewards, advantages, policy loss, KL | | **SFT** | loss, perplexity, accuracy | | **ORPO** | sft_loss, odds_ratio_loss, log_odds_ratio | | **KTO** | kl, logps for desirable/undesirable | ### 2. Crash Postmortems If training crashes or gets interrupted, you get a `postmortem.json` with full context: ```json { "exit_reason": "exception", "last_step": 847, "timestamp": "2025-12-17T19:26:04Z", "final_metrics": {"dpo_loss": 0.693, "win_rate": 0.52} } ``` No more "what step did it die on?" ### 3. Auto-Stop on Critical Issues Enable automatic training termination when critical issues are detected: ```python callback = DiagnosticsCallback(stop_on_critical=True) ``` ## Distributed Training Works automatically with multi-GPU setups. Zero configuration needed: ```bash accelerate launch --num_processes 8 train.py ``` Automatically detects stragglers, aggregates metrics across ranks, and tracks memory balance. ================================================ FILE: docs/source/quickstart.md ================================================ # Quickstart TRL is a comprehensive library for post-training foundation models using techniques like Supervised Fine-Tuning (SFT), Group Relative Policy Optimization (GRPO), Direct Preference Optimization (DPO). ## Quick Examples Get started instantly with TRL's most popular trainers. Each example uses compact models for quick experimentation. ### Supervised Fine-Tuning ```python from trl import SFTTrainer from datasets import load_dataset trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B", train_dataset=load_dataset("trl-lib/Capybara", split="train"), ) trainer.train() ``` ### Group Relative Policy Optimization ```python from trl import GRPOTrainer from datasets import load_dataset from trl.rewards import accuracy_reward trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=load_dataset("trl-lib/DeepMath-103K", split="train"), reward_funcs=accuracy_reward, ) trainer.train() ``` ### Direct Preference Optimization ```python from trl import DPOTrainer from datasets import load_dataset trainer = DPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=load_dataset("trl-lib/ultrafeedback_binarized", split="train"), ) trainer.train() ``` ### Reward Modeling ```python from trl import RewardTrainer from datasets import load_dataset dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = RewardTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=dataset, ) trainer.train() ``` ## Command Line Interface Skip the code entirely - train directly from your terminal: ```bash # SFT: Fine-tune on instructions trl sft --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/Capybara # DPO: Align with preferences trl dpo --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized # Reward: Train a reward model trl reward --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized ``` ## What's Next? ### 📚 Learn More - [SFT Trainer](sft_trainer) - Complete SFT guide - [DPO Trainer](dpo_trainer) - Preference alignment - [GRPO Trainer](grpo_trainer) - Group relative policy optimization ### 🚀 Scale Up - [Distributed Training](distributing_training) - Multi-GPU setups - [Memory Optimization](reducing_memory_usage) - Efficient training - [PEFT Integration](peft_integration) - LoRA and QLoRA ### 💡 Examples - [Example Scripts](https://github.com/huggingface/trl/tree/main/examples) - Production-ready code - [Community Tutorials](community_tutorials) - External guides ## Troubleshooting ### Out of Memory? Reduce batch size and enable optimizations: ```python training_args = SFTConfig( per_device_train_batch_size=1, # Start small gradient_accumulation_steps=8, # Maintain effective batch size ) ``` ```python training_args = DPOConfig( per_device_train_batch_size=1, # Start small gradient_accumulation_steps=8, # Maintain effective batch size ) ``` ### Loss not decreasing? Try adjusting the learning rate: ```python training_args = SFTConfig(learning_rate=2e-5) # Good starting point ``` For more help, open an [issue on GitHub](https://github.com/huggingface/trl/issues). ================================================ FILE: docs/source/rapidfire_integration.md ================================================ # RapidFire AI Integration RapidFire AI is an open-source experiment execution framework that enables concurrent training of multiple TRL configurations on the same GPU(s) through intelligent chunk-based scheduling. ## Key Features - **16-24× higher experimentation throughput** compared to sequential training. - **Almost no code changes** - drop-in configuration wrappers around TRL's and PEFT's existing configs. - **Interactive Control Operations** - real-time control to stop, resume, clone, and modify training runs in flight - **Automatic multi-GPU orchestration** with intelligent scheduling - **Full compatibility** with transformers, PEFT, SFTTrainer, DPOTrainer, and GRPOTrainer - **Full MLflow Integration**: Automatic experiment tracking and visualization - **Production-Ready**: Already used in production environments with complete working examples. ### Problem It Solves When fine-tuning or post-training with TRL, AI developers often need to: - Try different hyperparameter configurations - Compare different LoRA settings - Test different prompt schemes - Run ablation studies **Current approach**: Train each config one after another → slow and inefficient process **With RapidFire AI**: Train all configs in one go even on a single GPU → 16-24× faster process ### How It Works RapidFire AI employs **adaptive chunk-based scheduling**: ``` GPU Timeline (Single GPU): Chunk 1: [Config A] → [Config B] → [Config C] → [Config D] Chunk 2: [Config A] → [Config B] → [Config C] → [Config D] Chunk 3: [Config A] → [Config B] → [Config C] → [Config D] ``` This enables: - Early comparison of configurations on same data subsets incrementally - Efficient GPU utilization and minimizing idle times - Real-time and automated experiment metrics tracking - Dynamic control over runs in flight to incentivize more experimentation ## Installation ### Prerequisites - Python 3.12.x - NVIDIA GPU with Compute Capability 7.x or 8.x - CUDA Toolkit 11.8+ - PyTorch 2.7.1+ ### pip install ```bash pip install rapidfireai ``` Once installed, authenticate with Hugging Face and initialize RapidFire AI: ```bash # Authenticate with Hugging Face huggingface-cli login --token YOUR_TOKEN # Workaround for current issue: https://github.com/huggingface/xet-core/issues/527 pip uninstall -y hf-xet # Initialize RapidFire AI rapidfireai init # Start the RapidFire AI server rapidfireai start ``` The dashboard will be available at `http://0.0.0.0:3000` where you can monitor and control experiments in real-time. ## Quick Start: SFT Training with Multiple Configs Here's a complete example showing how to train multiple SFT configurations concurrently: ```python from rapidfireai import Experiment from rapidfireai.automl import List, RFGridSearch, RFModelConfig, RFLoraConfig, RFSFTConfig from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer # Load dataset dataset = load_dataset("bitext/Bitext-customer-support-llm-chatbot-training-dataset") train_dataset = dataset["train"].select(range(128)).shuffle(seed=42) eval_dataset = dataset["train"].select(range(100, 124)).shuffle(seed=42) # Define data formatting function def formatting_function(row): return { "prompt": [ {"role": "system", "content": "You are a helpful customer support assistant."}, {"role": "user", "content": row["instruction"]}, ], "completion": [ {"role": "assistant", "content": row["response"]} ] } # Initialize experiment experiment = Experiment(experiment_name="sft-customer-support") # Define multiple LoRA configurations to compare peft_configs = List([ RFLoraConfig(r=8, lora_alpha=16, lora_dropout=0.1, target_modules=["q_proj", "v_proj"], bias="none"), RFLoraConfig(r=32, lora_alpha=64, lora_dropout=0.1, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], bias="none") ]) # Define multiple training configurations # 2 base configs × 2 PEFT configs = 4 total training runs config_set = List([ RFModelConfig( model_name="TinyLlama/TinyLlama-1.1B-Chat-v1.0", peft_config=peft_configs, training_args=RFSFTConfig( # Wraps TRL's SFTConfig learning_rate=1e-3, per_device_train_batch_size=4, max_steps=128, fp16=True, ), model_type="causal_lm", model_kwargs={"device_map": "auto", "torch_dtype": "auto", "use_cache": False}, formatting_func=formatting_function, ), RFModelConfig( model_name="TinyLlama/TinyLlama-1.1B-Chat-v1.0", peft_config=peft_configs, training_args=RFSFTConfig( learning_rate=1e-4, # Different learning rate per_device_train_batch_size=4, max_steps=128, fp16=True, ), model_type="causal_lm", model_kwargs={"device_map": "auto", "torch_dtype": "auto", "use_cache": False}, formatting_func=formatting_function, ) ]) # Define model creation function def create_model(model_config): model = AutoModelForCausalLM.from_pretrained( model_config["model_name"], **model_config["model_kwargs"] ) tokenizer = AutoTokenizer.from_pretrained(model_config["model_name"]) return (model, tokenizer) # Create grid search over all configurations config_group = RFGridSearch(configs=config_set, trainer_type="SFT") # Run all 4 configurations concurrently with chunk-based scheduling experiment.run_fit(config_group, create_model, train_dataset, eval_dataset, num_chunks=4, seed=42) # End experiment experiment.end() ``` ### What Happens During Execution When you run this example: 1. **Config Expansion**: 2 base configurations × 2 PEFT configs = 4 total training runs 2. **Chunk-based Scheduling**: Training data is divided into chunks, and all 4 configs train concurrently 3. **GPU Swapping**: Models are swapped in/out of GPU memory based on chunk boundaries 4. **Real-time Tracking**: All metrics visible in the dashboard at `http://localhost:3000` 5. **Interactive Control**: Stop, resume, or clone any configuration from the dashboard This delivers **16-24× higher throughput** compared to training each configuration sequentially! ## Supported TRL Trainers ### SFTTrainer Use `RFSFTConfig` as a drop-in replacement for `SFTConfig`: ```python from rapidfireai.automl import RFSFTConfig training_args = RFSFTConfig( learning_rate=5e-5, per_device_train_batch_size=4, num_train_epochs=3, max_length = 512, # ... all other SFTConfig parameters supported ) ``` **Example Notebook**: [SFT for Customer Support](https://github.com/RapidFireAI/rapidfireai/blob/main/tutorial_notebooks/rf-tutorial-sft-chatqa-lite.ipynb) ### DPOTrainer Use `RFDPOConfig` as a drop-in replacement for `DPOConfig`: ```python from rapidfireai.automl import RFDPOConfig training_args = RFDPOConfig( beta=0.1, loss_type="sigmoid", max_length=1024, learning_rate=5e-4, # ... all other DPOConfig parameters supported ) ``` **Example Notebook**: [DPO for Preference Alignment](https://github.com/RapidFireAI/rapidfireai/blob/main/tutorial_notebooks/rf-tutorial-dpo-alignment-lite.ipynb) ### GRPOTrainer Use `RFGRPOConfig` as a drop-in replacement for `GRPOConfig`: ```python from rapidfireai.automl import RFGRPOConfig training_args = RFGRPOConfig( learning_rate=5e-6, num_generations=8, max_completion_length=256, # ... all other GRPOConfig parameters supported ) ``` **Example Notebook**: [GRPO for Math Reasoning](https://github.com/RapidFireAI/rapidfireai/blob/main/tutorial_notebooks/rf-tutorial-grpo-mathreasoning-lite.ipynb) ## Core Concepts ### Chunk-Based Concurrent Training RapidFire AI divides training data into chunks and alternates between configurations: ``` GPU Timeline (Single GPU): Chunk 1: [Config A] → [Config B] → [Config C] → [Config D] Chunk 2: [Config A] → [Config B] → [Config C] → [Config D] Chunk 3: [Config A] → [Config B] → [Config C] → [Config D] ... ``` This approach maximizes GPU utilization and enables early comparison of configurations while maintaining training stability through automatic checkpointing. ### Interactive Control Operations (IC Ops) Through the RapidFire AI dashboard, you can dynamically control running experiments: - **Stop**: Pause a configuration (checkpointed automatically) - **Resume**: Continue from last checkpoint - **Clone**: Duplicate a configuration with modifications - **Clone & Warm Start**: Clone and initialize from parent's weights - **Delete**: Remove failed or unwanted runs This enables adaptive experimentation where you can stop underperforming configs early and clone promising ones with tweaked hyperparameters. ### Multi-Config Experimentation Use `RFGridSearch` or `RFRandomSearch` to automatically generate configuration combinations: ```python # Grid search: tests all combinations config_group = RFGridSearch(configs=config_list, trainer_type="SFT") # Random search: samples N configurations config_group = RFRandomSearch(configs=config_list, trainer_type="DPO", num_samples=10) ``` ## Advanced Features ### PEFT/LoRA Integration Full support for parameter-efficient fine-tuning: ```python from rapidfireai.automl import RFLoraConfig from peft import TaskType lora_config = RFLoraConfig( task_type=TaskType.CAUSAL_LM, r=64, lora_alpha=64, lora_dropout=0.1, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], bias="none" ) ``` ### Custom Reward Functions (GRPO) Define multiple reward functions for GRPO training: ```python def correctness_reward(prompts, completions, answer, **kwargs): """Reward for correct answers""" responses = [completion[0]['content'] for completion in completions] extracted = [extract_answer(r) for r in responses] return [2.0 if r == a else 0.0 for r, a in zip(extracted, answer)] def format_reward(completions, **kwargs): """Reward for proper formatting""" import re pattern = r".*?\s*.*?" responses = [completion[0]["content"] for completion in completions] matches = [re.match(pattern, r) for r in responses] return [0.5 if match else 0.0 for match in matches] # Use in model config config = RFModelConfig( reward_funcs=[correctness_reward, format_reward], # ... other parameters ) ``` ### Multi-GPU Support RapidFire AI automatically detects and utilizes all available GPUs. No special configuration needed - the scheduler automatically distributes configurations across GPUs. ## Best Practices ### Tuning Chunk Granularity The `num_chunks` parameter controls swap frequency: ```python # Fewer chunks = less overhead, less frequent comparison experiment.run_fit(..., num_chunks=2) # More chunks = more overhead, more frequent comparison experiment.run_fit(..., num_chunks=16) ``` **Rule of thumb**: Start with `num_chunks=4` and adjust based on dataset size and number of configurations. ### Memory Management For large models, use quantization: ```python from transformers import BitsAndBytesConfig import torch bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", ) model_kwargs = { "quantization_config": bnb_config, "device_map": "auto", } ``` ## Performance Benchmarks Based on internal benchmarks comparing sequential vs. RapidFire AI concurrent training: | Scenario | Sequential Time | RapidFire AI Time | Speedup | |----------|----------------|-------------------|---------| | 4 configs, 1 GPU | 120 min | 7.5 min | 16× | | 8 configs, 1 GPU | 240 min | 12 min | 20× | | 4 configs, 2 GPUs | 60 min | 4 min | 15× | | 8 configs, 4 GPUs | 60 min | 3 min | 20× | *Benchmarks performed on NVIDIA A100 40GB with TinyLlama-1.1B and Llama-3.2-1B models* ## Troubleshooting For troubleshooting guidance, see the [RapidFire AI Troubleshooting Guide](https://oss-docs.rapidfire.ai/en/latest/troubleshooting.html). ## Additional Resources - **Colab Notebook**: [RapidFire AI in Google Colab](http://tinyurl.com/rapidfireai-colab) - **Documentation**: [oss-docs.rapidfire.ai](https://oss-docs.rapidfire.ai) - **GitHub**: [RapidFireAI/rapidfireai](https://github.com/RapidFireAI/rapidfireai) - **PyPI**: [pypi.org/project/rapidfireai](https://pypi.org/project/rapidfireai/) - **Discord**: [Join our Discord](https://discord.gg/6vSTtncKNN) - **Tutorial Notebooks**: [GitHub Repository](https://github.com/RapidFireAI/rapidfireai/tree/main/tutorial_notebooks) Learn more about RapidFire AI in their [official repository](https://github.com/RapidFireAI/rapidfireai) and [documentation](https://oss-docs.rapidfire.ai). ================================================ FILE: docs/source/reducing_memory_usage.md ================================================ # Reducing Memory Usage Training workflows can often be optimized to **reduce memory consumption**, and TRL provides several built-in features to help achieve this. Below, we outline these techniques and recommend experimenting with different combinations to figure out which configuration works best for your specific setup. Each method includes examples for the supported trainers. If you're unsure whether a technique is compatible with your trainer, please take a look at the corresponding trainer documentation. For additional strategies, such as **gradient checkpointing**, which is supported across all trainers, see the [`transformers` performance guide](https://huggingface.co/docs/transformers/perf_train_gpu_one#gradient-checkpointing). ## Truncation Sequence lengths in the dataset can vary widely. When data is batched, sequences are padded to match the longest one in the batch, which can cause high memory usage, even if most sequences are relatively short. ![Truncation prompt-completion](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/why_you_should_truncate.png) To reduce memory usage, it's important to truncate sequences to a reasonable length. While TRL trainers truncate sequences by default, you may want to adjust the default truncation length to better align with your specific use case. DPO truncation is controlled via `max_length`, which truncates the combined prompt+completion sequence. ![DPO truncation](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/truncation_prompt_completion.png) To set the truncation parameter, use the following code snippet: ```python from trl import DPOConfig training_args = DPOConfig(..., max_length=...) ``` > [!WARNING] > The legacy `max_prompt_length` and `max_completion_length` parameters are now removed; instead, filter or pre-truncate overlong prompts/completions in your dataset before training. SFT truncation is applied to the input sequence via the `max_length` parameter. ![Truncation input ids](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/truncation_input_ids.png) To set the truncation parameter, use the following code snippet: ```python from trl import SFTConfig training_args = SFTConfig(..., max_length=...) ``` ### How to choose the `max_length` value? If `max_length` is too small, a significant portion of your tokens will be discarded and won't contribute to training. If it's too large, memory usage can spike, potentially leading to out-of-memory (OOM) errors. Without packing or padding-free, a large `max_length` may also result in inefficient training, as many tokens will be padding. To help you choose an appropriate value, we provide a utility to visualize the sequence length distribution in your dataset. ## Packing > [!TIP] > This technique is available only for **SFT** training and setups that use **FlashAttention** (or its variants). [Truncation](#truncation) has several drawbacks: 1. **Loss of information**: Important tokens at the end of sequences may be discarded. 2. **Choosing truncation length**: Too short loses data; too long reduces efficiency. Packing mitigates these issues by grouping multiple sequences into the same training row, filling each row up to `max_length`. ![Packing](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/packing_3.png) TRL implements packing using **Best-Fit Decreasing (BFD)** bin packing, which groups sequences efficiently while minimizing padding. When a sequence exceeds `max_length`, different strategies determine how the overflow tokens are handled. TRL supports three strategies: * `"bfd"` (default): Uses **Best-Fit Decreasing packing**. If a sequence exceeds `max_length`, the overflow tokens are discarded. * `"bfd_split"`: Uses **Best-Fit Decreasing packing**, but long sequences are split into chunks ≤ `max_length` before packing. This preserves all tokens and follows the approach proposed in [Fewer Truncations Improve Language Modeling](https://huggingface.co/papers/2404.10830). * `"wrapped"`: All tokens are concatenated into a stream and split into fixed-length blocks. This minimizes padding but may mix unrelated examples. This strategy corresponds to the *concatenate-then-split* preprocessing described in the literature (e.g., [Fewer Truncations Improve Language Modeling](https://huggingface.co/papers/2404.10830)). It has the downside of breaking sequence continuity for a large fraction of the dataset, which hurts performance, as discussed in the [Qwen3-Coder-Next Technical Report](https://huggingface.co/papers/2603.00729). > [!NOTE] > If all sequences are shorter than `max_length`, **`bfd` and `bfd_split` behave identically**, since no truncation or splitting is required. ```python from trl import SFTConfig training_args = SFTConfig( ..., packing=True, packing_strategy="bfd", max_length=512, ) ``` ## PEFT for parameter-efficient fine-tuning Parameter-Efficient Fine-Tuning (PEFT) methods like LoRA are among the most effective techniques for reducing memory usage during training. Instead of training all model parameters, PEFT methods train only a small number of adapter parameters, significantly reducing memory requirements and enabling fine-tuning of larger models on limited hardware. For comprehensive details on using PEFT with TRL, including various adapter methods, quantization options, and advanced configurations, see [PEFT Integration](peft_integration). To use PEFT for reducing memory usage: ```python from datasets import load_dataset from peft import LoraConfig from trl import SFTTrainer dataset = load_dataset("trl-lib/Capybara", split="train") peft_config = LoraConfig() trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B", train_dataset=dataset, peft_config=peft_config, ) ``` PEFT can be combined with other memory reduction techniques such as quantization (4-bit or 8-bit) for even greater memory savings. See [PEFT Integration](peft_integration) for quantization examples. ## Liger for reducing peak memory usage [Liger Kernel](https://github.com/linkedin/Liger-Kernel) is a collection of Triton kernels designed specifically for LLM training. It can effectively increase multi-GPU training throughput by 20% and reduce memory usage by 60%. For more information, see [Liger Kernel Integration](liger_kernel_integration). To use Liger for reducing peak memory usage, use the following code snippet: ```python from trl import SFTConfig training_args = SFTConfig(..., use_liger_kernel=True) ``` ```python from trl import DPOConfig training_args = DPOConfig(..., use_liger_kernel=True) ``` ```python from trl import GRPOConfig training_args = GRPOConfig(..., use_liger_kernel=True) ``` ```python from trl.experimental.kto import KTOConfig training_args = KTOConfig(..., use_liger_kernel=True) ``` ```python from trl.experimental.gkd import GKDConfig training_args = GKDConfig(..., use_liger_kernel=True) ``` ## Padding-free Padding-free batching is an alternative approach for reducing memory usage. In this method, a batch is first sampled and then flattened into a single sequence, avoiding padding. Unlike packing, which can result in incomplete sequences by combining parts of different samples, padding-free batching ensures that all sequences remain complete and intact. ![Padding-free](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/padding-free.png) > [!WARNING] > It's highly recommended to use padding-free batching with **FlashAttention 2** or **FlashAttention 3**. Otherwise, you may encounter batch contamination issues. ```python from trl import DPOConfig training_args = DPOConfig(..., padding_free=True, model_init_kwargs={"attn_implementation": "kernels-community/flash-attn2"}) ``` ```python from trl import SFTConfig training_args = SFTConfig(..., padding_free=True, model_init_kwargs={"attn_implementation": "kernels-community/flash-attn2"}) ``` ## Activation offloading Activation offloading is a memory efficiency technique that reduces GPU VRAM usage by temporarily moving activation tensors to CPU RAM during the forward pass and bringing them back only when needed for the backward pass. This significantly reduces peak memory usage at the cost of slightly increased training time. To enable activation offloading in your SFT training configuration: ```python from trl import SFTConfig training_args = SFTConfig(..., activation_offloading=True) ``` Under the hood, activation offloading implements PyTorch's [`saved_tensors_hooks`](https://pytorch.org/tutorials/intermediate/autograd_saved_tensors_hooks_tutorial.html#hooks-for-autograd-saved-tensors) to intercept activations during the forward pass. It intelligently manages which tensors to offload based on size and context, avoiding offloading output tensors that would be inefficient. For performance optimization, it can, via a flag (which is true by default), use CUDA streams to overlap computation with CPU-GPU transfers. ## Padding Sequences to a Multiple > [!TIP] > This technique is supported for **SFT** and **Reward** trainers currently. When enabled, this option ensures that all sequences are **padded to a multiple** of the specified value. This can improve computational efficiency on some hardware by aligning sequence lengths to memory-friendly boundaries. ```python from trl import SFTConfig training_args = SFTConfig(..., pad_to_multiple_of=2048) ``` ```python from trl import RewardConfig training_args = RewardConfig(..., pad_to_multiple_of=2048) ``` ## Disabling model gathering for generation in online methods When using DeepSpeed ZeRO-3, model weights are sharded across multiple GPUs. Online methods involve generating completions from the model as part of the training process. During this step, the model weights are temporarily gathered on a single GPU for generation. For very large models, this gathering can lead to OOM errors, as described in this issue: [#2250](https://github.com/huggingface/trl/issues/2250#issue-2598304204). If you encounter this issue, you can disable the gathering of model weights for generation by setting the following parameter: ```python from trl import GRPOConfig training_args = GRPOConfig(..., ds3_gather_for_generation=False) ``` ```python from trl.experimental.online_dpo import OnlineDPOConfig training_args = OnlineDPOConfig(..., ds3_gather_for_generation=False) ``` ```python from trl.experimental.ppo import PPOConfig training_args = PPOConfig(..., ds3_gather_for_generation=False) ``` ```python from trl import RLOOConfig training_args = RLOOConfig(..., ds3_gather_for_generation=False) ``` This adjustment prevents model weights from being gathered, avoiding OOM errors, but it may result in slower generation speeds. ## vLLM sleep mode When using **vLLM** as the generation backend for online training methods, you can enable _sleep mode_ to offload vLLM parameters and cache to CPU RAM during the optimization step and reload them back to GPU VRAM when needed for weight synchronization and generation. ```python from trl import GRPOConfig training_args = GRPOConfig(..., vllm_enable_sleep_mode=True) ``` ```python from trl import RLOOConfig training_args = RLOOConfig(..., vllm_enable_sleep_mode=True) ``` Offloading the vLLM weights and cache helps keep GPU memory usage low, which can be particularly beneficial when training large models or using limited GPU resources. However, waking the vLLM engine from sleep mode introduces some host–device transfer latency, which may slightly impact training speed. ## Gradient checkpointing Gradient checkpointing trades compute for memory by not storing all intermediate activations during the forward pass, recomputing them during the backward pass instead. ```python from trl import SFTConfig training_args = SFTConfig(..., gradient_checkpointing=True) ``` > [!NOTE] > Gradient checkpointing is enabled by default in all trainers to optimize memory usage. You can disable it by setting `gradient_checkpointing=False` if needed. For more memory optimization techniques, see the [Transformers Performance Guide](https://huggingface.co/docs/transformers/perf_train_gpu_one#gradient-checkpointing). ================================================ FILE: docs/source/reward_trainer.md ================================================ # Reward Modeling [![model badge](https://img.shields.io/badge/All_models-Reward_Trainer-blue)](https://huggingface.co/models?other=reward-trainer,trl) ## Overview TRL supports the Outcome-supervised Reward Modeling (ORM) Trainer for training reward models. This post-training method was contributed by [Younes Belkada](https://huggingface.co/ybelkada). ## Quick start This example demonstrates how to train a reward model using the [`RewardTrainer`] from TRL. We train a [Qwen 3 0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) model on the [UltraFeedback dataset](https://huggingface.co/datasets/trl-lib/ultrafeedback_binarized), large-scale, fine-grained, diverse preference dataset. ```python from trl import RewardTrainer from datasets import load_dataset trainer = RewardTrainer( model="Qwen/Qwen3-0.6B", train_dataset=load_dataset("trl-lib/ultrafeedback_binarized", split="train"), ) trainer.train() ``` ## Expected dataset type and format [`RewardTrainer`] supports [preference](dataset_formats#preference) datasets type (both implicit and explicit prompt). The [`RewardTrainer`] is compatible with both [standard](dataset_formats#standard) and [conversational](dataset_formats#conversational) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ```python # Standard preference (implicit prompt) {"chosen": "The sky is blue.", "rejected": "The sky is green."} # Conversational preference (implicit prompt) {"chosen": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}]} # Standard preference (explicit prompt) {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."} # Conversational preference (explicit prompt) {"prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}]} ``` If your dataset is not in one of these formats, you can preprocess it to convert it into the expected format. Here is an example with the [lmarena-ai/arena-human-preference-55k](https://huggingface.co/datasets/lmarena-ai/arena-human-preference-55k) dataset: ```python from datasets import load_dataset import json dataset = load_dataset("lmarena-ai/arena-human-preference-55k") # Filter out ties dataset = dataset.filter(lambda example: example["winner_tie"] == 0) # Create 'chosen' and 'rejected' fields based on the winner column def response_a_b_to_chosen_rejected(example): if example["winner_model_a"] == 1: example["chosen"] = example["response_a"] example["rejected"] = example["response_b"] else: example["chosen"] = example["response_b"] example["rejected"] = example["response_a"] return example dataset = dataset.map(response_a_b_to_chosen_rejected) # Convert to conversational format def make_conversation(example): prompt = json.loads(example["prompt"])[0] # '["What color is the sky?"]' -> "What color is the sky?" chosen = json.loads(example["chosen"])[0] rejected = json.loads(example["rejected"])[0] return { "chosen": [{"role": "user", "content": prompt}, {"role": "assistant", "content": chosen}], "rejected": [{"role": "user", "content": prompt}, {"role": "assistant", "content": rejected}], } dataset = dataset.map(make_conversation) # Keep only necessary columns dataset = dataset.select_columns(["chosen", "rejected"]) print(next(iter(dataset["train"]))) ``` ```json { "chosen": [ {"role": "user", "content": "Is it morally right to try to have a certain percentage of females on managerial positions?"}, {"role": "assistant", "content": "The question of whether it is morally right to aim for a certain percentage of females..."}, ], "rejected": [ {"role": "user", "content": "Is it morally right to try to have a certain percentage of females on managerial positions?"}, {"role": "assistant", "content": "As an AI, I don't have personal beliefs or opinions. However, ..."}, ], } ``` ## Looking deeper into the training method Reward Models (RMs) are typically trained using supervised learning on datasets containing pairs of preferred and non-preferred responses. The goal is to learn a function that assigns higher scores to preferred responses, enabling the model to rank outputs based on preferences. This section breaks down how reward modeling works in practice, covering the key steps: **preprocessing** and **loss computation**. ### Preprocessing and tokenization During training, each example is expected to contain a **chosen** and **rejected** field. For more details on the expected formats, see [Dataset formats - Preference](dataset_formats#preference). The [`RewardTrainer`] tokenizes each input using the model's tokenizer. If prompts and completions (chosen and rejected) are provided separately (explicit prompt case), they are concatenated before tokenization. ### Computing the loss Let \\( x \\) be the input sequence (prompt) and \\( y^+ \\) and \\( y^- \\) be the chosen and rejected sequences respectively. Under the Bradley-Terry model ([Bradley & Terry, 1952](https://www.jstor.org/stable/2334029)), the probability that \\( y^+ \\) is preferred over \\( y^- \\) given a reward function \\( r \\) is \\( p(y^+ ≻ y^- |x) = \sigma(r(x, y^+)−r(x, y^-)) \\), where \\( σ \\) is the sigmoid function. The reward model \\( r_\theta(x, y) \\) is trained to assign higher scores to preferred responses \\( y^+ \\) over non-preferred ones \\( y^- \\). The loss is then defined as the negative log-likelihood of the observed preferences: $$ \mathcal{L}(\theta) = - \mathbb{E}_{(x,y^+,y^-) \sim \mathcal{D}} \left[ \log \sigma(r_\theta(x, y^+) - r_\theta(x, y^-)) \right]. $$ > [!TIP] > The Bradley-Terry model is underdetermined, meaning that adding a constant to all rewards does not change the preference probabilities. To address this, [Helping or Herding? Reward Model Ensembles Mitigate but do not Eliminate Reward Hacking](https://huggingface.co/papers/2312.09244) proposes adding an auxiliary loss term that encourages the rewards to be centered around zero. This is controlled by the `center_rewards_coefficient` parameter in the [`RewardConfig`]. The recommended value is `1e-2`. ## Logged metrics While training and evaluating we record the following reward metrics: * `global_step`: The total number of optimizer steps taken so far. * `epoch`: The current epoch number, based on dataset iteration. * `num_tokens`: The total number of tokens processed so far. * `loss`: The average loss over the last logging interval. * `accuracy`: The proportion of correct predictions (i.e., the model assigned a higher score to the chosen response than to the rejected one) averaged over the last logging interval. * `min_reward`: The minimum reward score assigned by the model. This value is averaged over the logging interval. * `mean_reward`: The average reward score assigned by the model over the last logging interval. * `max_reward`: The maximum reward score assigned by the model. This value is averaged over the logging interval. * `margin`: The average margin (difference between chosen and rejected rewards) over the last logging interval. * `learning_rate`: The current learning rate, which may change dynamically if a scheduler is used. * `grad_norm`: The L2 norm of the gradients, computed before gradient clipping. ## Customization ### Model initialization You can directly pass the kwargs of the [`~transformers.AutoModelForSequenceClassification.from_pretrained()`] method to the [`RewardConfig`]. For example, if you want to load a model in a different precision, analogous to ```python model = AutoModelForSequenceClassification.from_pretrained("Qwen/Qwen3-0.6B", dtype=torch.bfloat16) ``` you can do so by passing the `model_init_kwargs={"dtype": torch.bfloat16}` argument to the [`RewardConfig`]. ```python from trl import RewardConfig training_args = RewardConfig( model_init_kwargs={"dtype": torch.bfloat16}, ) ``` Note that all keyword arguments of [`~transformers.AutoModelForSequenceClassification.from_pretrained()`] are supported, except for `num_labels`, which is automatically set to 1. ### Train adapters with PEFT We support tight integration with 🤗 PEFT library, allowing any user to conveniently train adapters and share them on the Hub, rather than training the entire model. ```python from datasets import load_dataset from trl import RewardTrainer from peft import LoraConfig dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = RewardTrainer( "Qwen/Qwen3-4B", train_dataset=dataset, peft_config=LoraConfig(modules_to_save=["score"]) # important to include the score head when base model is not a sequence classification model ) trainer.train() ``` You can also continue training your [`~peft.PeftModel`]. For that, first load a `PeftModel` outside [`RewardTrainer`] and pass it directly to the trainer without the `peft_config` argument being passed. ```python from datasets import load_dataset from trl import RewardTrainer from peft import AutoPeftModelForCausalLM model = AutoPeftModelForCausalLM.from_pretrained("trl-lib/Qwen3-4B-Reward-LoRA", is_trainable=True) dataset = load_dataset("trl-lib/Capybara", split="train") trainer = RewardTrainer( model=model, train_dataset=dataset, ) trainer.train() ``` > [!TIP] > When training adapters, you typically use a higher learning rate (≈1e‑3) since only new parameters are being learned. > > ```python > RewardConfig(learning_rate=1e-3, ...) > ``` ## Tool Calling with Reward Modeling The [`RewardTrainer`] fully supports fine-tuning models with _tool calling_ capabilities. In this case, each dataset example should include: * The conversation messages, including any tool calls (`tool_calls`) and tool responses (`tool` role messages) * The list of available tools in the `tools` column, typically provided as JSON schemas For details on the expected dataset structure, see the [Dataset Format — Tool Calling](dataset_formats#tool-calling) section. ## RewardTrainer [[autodoc]] RewardTrainer - train - save_model - push_to_hub ## RewardConfig [[autodoc]] RewardConfig ================================================ FILE: docs/source/rewards.md ================================================ # Reward Functions This module contains some useful reward functions, primarily intended for use with the [`GRPOTrainer`] and [`RLOOTrainer`]. ## accuracy_reward [[autodoc]] rewards.accuracy_reward ## reasoning_accuracy_reward [[autodoc]] rewards.reasoning_accuracy_reward ## think_format_reward [[autodoc]] rewards.think_format_reward ## get_soft_overlong_punishment [[autodoc]] rewards.get_soft_overlong_punishment ================================================ FILE: docs/source/rloo_trainer.md ================================================ # RLOO Trainer [![model badge](https://img.shields.io/badge/All_models-RLOO-blue)](https://huggingface.co/models?other=rloo,trl) ## Overview TRL supports the RLOO Trainer for training language models, as described in the paper [Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs](https://huggingface.co/papers/2402.14740) by [Arash Ahmadian](https://huggingface.co/ArashAhmadian), Chris Cremer, [Matthias Gallé](https://huggingface.co/mgalle), [Marzieh Fadaee](https://huggingface.co/MarziehFadaee), [Julia Kreutzer](https://huggingface.co/JuliaKreutzerCohere), [Ahmet Üstün](https://huggingface.co/ahmetu) and [Sara Hooker](https://huggingface.co/sarahooker). The abstract from the paper is the following: > AI alignment in the shape of Reinforcement Learning from Human Feedback (RLHF) is increasingly treated as a crucial ingredient for high performance large language models. Proximal Policy Optimization (PPO) has been positioned by recent literature as the canonical method for the RL part of RLHF However, it involves both high computational cost and sensitive hyperparameter tuning. We posit that most of the motivational principles that led to the development of PPO are less of a practical concern in RLHF and advocate for a less computationally expensive method that preserves and even increases performance. We revisit the formulation of alignment from human preferences in the context of RL. Keeping simplicity as a guiding principle, we show that many components of PPO are unnecessary in an RLHF context and that far simpler REINFORCE-style optimization variants outperform both PPO and newly proposed “RL-free” methods such as DPO and RAFT. Our work suggests that careful adaptation to LLMs alignment characteristics enables benefiting from online RL optimization at low cost. This post-training method was contributed by [Costa Huang](https://github.com/vwxyzjn) and later refactored by [Shirin Yamani](https://huggingface.co/ShirinYamani). ## Quick start This example demonstrates how to train a model using the RLOO method. We train a [Qwen 0.5B Instruct model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) with the prompts from the [DeepMath-103K dataset](https://huggingface.co/datasets/trl-lib/DeepMath-103K). You can view the data in the dataset here: Below is the script to train the model. ```python # train_rloo.py from datasets import load_dataset from trl import RLOOTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = RLOOTrainer( model="Qwen/Qwen2-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_rloo.py ``` ## Looking deeper into the RLOO method RLOO is an online learning algorithm, meaning it improves iteratively by using the data generated by the trained model itself during training. The intuition behind RLOO objective is to maximize the advantage of the generated completions, while ensuring that the model remains close to the reference policy. To understand how RLOO works, it can be broken down into four main steps: **Generating completions**, **computing the advantage**, **estimating the KL divergence**, and **computing the loss**. ![RLOO](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/rloo.png) ### Generating completions At each training step, we sample a batch of prompts and generate a set of \\( G \\) completions for each prompt (denoted as \\( o_i \\)). ### Computing the reward In RLOO, the reward consists of two components: the reward provided by the reward model (or reward function) and a KL penalty that discourages the policy from deviating too far from a fixed reference policy 1. For each of the \\( G \\) generated sequences \\( o_i = (o_{i,1}, \dots, o_{i,T}) \\) conditioned on a query \\( q \\), we compute a scalar reward using a reward model \\( R(o_i, q) \\). 2. Concurrently, we estimate the KL divergence between the current policy \\( \pi_\theta \\) and the fixed reference policy \\( \pi_{\text{ref}} \\) over the sequence. The KL estimate for sequence \\( o_i \\) is: $$ \mathbb{D}_{\mathrm{KL}}\!\left[\pi_\theta\|\pi_{\mathrm{ref}}\right] = \sum_{t=1}^T \log \frac{\pi_\theta(o_{i,t} \mid q, o_{i, 0 \\) controls the strength of the KL penalty. > [!TIP] > In a purely online setting (`num_iterations = 1`, default), the data are generated by the current policy. In this case, the KL penalty is computed directly using the current policy. > > In the more general setting (e.g., multiple gradient steps per batch), the data are instead generated by an earlier snapshot \\( \pi_{\text{old}} \\). To keep the penalty consistent with the sampling distribution, the KL is defined with respect to this policy: > > $$ > \mathbb{D}_{\mathrm{KL}}\!\left[\pi_{\text{old}} \,\|\, \pi_{\text{ref}}\right]. > $$ > > Equivalently, for a sampled sequence $o$, the Monte Carlo estimate is > > $$ > \mathbb{D}_{\mathrm{KL}}\!\left[\pi_{\text{old}} \|\pi_{\mathrm{ref}}\right] = \sum_{t=1}^T \log \frac{\pi_{\text{old}}(o_{i,t} \mid q, o_{i, $$ ### Computing the advantage Once the rewards for each completion have been computed, we calculate a baseline as the average reward of all other samples in the same batch, excluding the current sample. This baseline is used to reduce the variance of the policy gradient estimate. The advantage for each completion is then obtained as the difference between its own reward and this leave-one-out baseline. Formally, for a batch of G completions, the baseline for completion is: $$ b_i = \frac{1}{G-1} \sum_{j \neq i} r_j $$ and then the advantage for each completion is computed as the difference between its reward and the baseline: $$ A_i = r_i - b_i $$ ### Computing the loss The REINFORCE loss is simply defined as: $$ \mathcal{L}_{\text{RLOO}}(\theta) = - \frac{1}{G} \sum_{i=1}^G \hat{A}_i \, \log \pi_\theta(o_i \mid q) $$ In practice, performing multiple gradient steps on the same batch makes the actions effectively off-policy relative to the current parameters. To correct for this, we introduce the importance sampling ratio. To prevent excessively large updates when the policy changes between sampling and gradient steps, we clip this ratio: $$ \mathcal{L}_{\text{RLOO}}(\theta) = - \frac{1}{G} \sum_{i=1}^G \min \left( \frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_\text{old}}(o_i \mid q)} \hat{A}_i, \, \text{clip}\left(\frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_\text{old}}(o_i \mid q)}, 1-\epsilon, 1+\epsilon\right) \hat{A}_i \right) $$ In a fully online, single-step setting (default), \\( \frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_\text{old}}(o_i \mid q)} = 1 \\) and this reduces to standard REINFORCE. ## Logged metrics While training and evaluating, we record the following reward metrics: - `num_tokens`: The total number of tokens processed so far, including both prompts and completions. - `step_time`: The average time (in seconds) taken per training step (including generation). - `completions/mean_length`: The average length of generated completions. - `completions/min_length`: The minimum length of generated completions. - `completions/max_length`: The maximum length of generated completions. - `completions/mean_terminated_length`: The average length of generated completions that terminate with EOS. - `completions/min_terminated_length`: The minimum length of generated completions that terminate with EOS. - `completions/max_terminated_length`: The maximum length of generated completions that terminate with EOS. - `completions/clipped_ratio`: The ratio of truncated (clipped) completions. - `reward/{reward_func_name}/mean`: The average reward from a specific reward function. - `reward/{reward_func_name}/std`: The standard deviation of the reward from a specific reward function. - `reward`: The overall average reward after summing rewards across functions (unweighted). - `reward_std`: The standard deviation of summed rewards across functions (unweighted), computed over the full batch. - `frac_reward_zero_std`: The fraction of samples in the generation batch with a reward std of zero, implying there is little diversity for that prompt (all answers are correct or incorrect). - `entropy`: Average entropy of token predictions across generated completions. (If `mask_truncated_completions=True`, masked sequences tokens are excluded.) - `kl`: The average KL divergence between the model and the reference model, calculated over generated completions. Logged only if `beta` is nonzero. - `clip_ratio/region_mean`: The ratio of sequence probabilities where the RLOO objective is clipped to stay within the trust region: \\( \text{clip}\left( r_{i}(\theta), 1 - \epsilon_\mathrm{low}, 1 + \epsilon_\mathrm{high} \right)\,, \quad r_{i}(\theta) = \frac{\pi_\theta(o_{i} \mid q)}{\pi_{\theta_{\text{old}}}(o_{i} \mid q)} \\). A higher value means more samples are clipped, which constrains how much the policy $\pi_\theta$ can change. - `clip_ratio/low_mean`: The average ratio of sequence probabilities that were clipped on the lower bound of the trust region: \\(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\\). - `clip_ratio/low_min`: The minimum ratio of sequence probabilities that were clipped on the lower bound of the trust region: \\(r_{i,t}(\theta) < 1 - \epsilon_\mathrm{low}\\). - `clip_ratio/high_mean`: The average ratio of sequence probabilities that were clipped on the upper bound of the trust region: \\(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\\). - `clip_ratio/high_max`: The maximum ratio of sequence probabilities that were clipped on the upper bound of the trust region: \\(r_{i,t}(\theta) > 1 + \epsilon_\mathrm{high}\\). ## Customization ### Speed up training with vLLM-powered generation Generation is often the main bottleneck when training with online methods. To accelerate generation, you can use [vLLM](https://github.com/vllm-project/vllm), a high-throughput, low-latency inference engine for LLMs. To enable it, first install the package with ```shell pip install trl[vllm] ``` We support two ways of using vLLM during training: **server mode** and **colocate mode**. #### Option 1: Colocate mode In this mode, vLLM runs inside the trainer process and shares GPU memory with the training model. This avoids launching a separate server and can improve GPU utilization, but may lead to memory contention on the training GPUs. This is the default mode. ```python from trl import RLOOConfig training_args = RLOOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` #### Option 2: Server mode In this mode, vLLM runs in a separate process (and using separate GPUs) and communicates with the trainer via HTTP. This is ideal if you have dedicated GPUs for inference. 1. **Start the vLLM server**: ```bash trl vllm-serve --model ``` 2. **Enable server mode in your training script**: ```python from trl import RLOOConfig training_args = RLOOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` > [!WARNING] > Make sure that the server is using different GPUs than the trainer, otherwise you may run into NCCL errors. You can specify the GPUs to use with the `CUDA_VISIBLE_DEVICES` environment variable. > [!TIP] > Depending on the model size and the overall GPU memory requirements for training, you may need to adjust the `vllm_gpu_memory_utilization` parameter in [`RLOOConfig`] to avoid underutilization or out-of-memory errors. > > We provide a [HF Space](https://huggingface.co/spaces/trl-lib/recommend-vllm-memory) to help estimate the recommended GPU memory utilization based on your model configuration and experiment settings. Simply use it as follows to get `vllm_gpu_memory_utilization` recommendation: > > > > If the recommended value does not work in your environment, we suggest adding a small buffer (e.g., +0.05 or +0.1) to the recommended value to ensure stability. > > If you still find you are getting out-of-memory errors set `vllm_enable_sleep_mode` to True and the vllm parameters and cache will be offloaded during the optimization step. For more information, see [Reducing Memory Usage with vLLM Sleep Mode](reducing_memory_usage#vllm-sleep-mode). > [!TIP] > By default, RLOO uses `MASTER_ADDR=localhost` and `MASTER_PORT=12345` for vLLM, but you can override these values by setting the environment variables accordingly. For more information, see [Speeding up training with vLLM](speeding_up_training#vllm-for-fast-generation-in-online-methods). ### RLOO at scale: train a 70B+ Model on multiple nodes When training large models like **Qwen2.5-72B**, you need several key optimizations to make the training efficient and scalable across multiple GPUs and nodes. These include: - **DeepSpeed ZeRO Stage 3**: ZeRO leverages data parallelism to distribute model states (weights, gradients, optimizer states) across multiple GPUs and CPUs, reducing memory and compute requirements on each device. Since large models cannot fit on a single GPU, using ZeRO Stage 3 is required for training such models. For more details, see [DeepSpeed Integration](deepspeed_integration). - **Accelerate**: Accelerate is a library that simplifies distributed training across multiple GPUs and nodes. It provides a simple API to launch distributed training and handles the complexities of distributed training, such as data parallelism, gradient accumulation, and distributed data loading. For more details, see [Distributing Training](distributing_training). - **vLLM**: See the previous section on how to use vLLM to speed up generation. Below is an example SLURM script to train a 70B model with RLOO on multiple nodes. This script trains a model on 4 nodes and uses the 5th node for vLLM-powered generation. ```sh #!/bin/bash #SBATCH --nodes=5 #SBATCH --gres=gpu:8 # Get the list of allocated nodes NODELIST=($(scontrol show hostnames $SLURM_JOB_NODELIST)) # Assign the first 4 nodes for training and the 5th node for vLLM TRAIN_NODES="${NODELIST[@]:0:4}" # Nodes 0, 1, 2, 3 for training VLLM_NODE="${NODELIST[4]}" # Node 4 for vLLM # Run training on the first 4 nodes (Group 1) srun --nodes=4 --ntasks=4 --nodelist="${NODELIST[@]:0:4}" accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ --num_processes 32 \ --num_machines 4 \ --main_process_ip ${NODELIST[0]} \ --machine_rank $SLURM_PROCID \ --rdzv_backend c10d \ train_rloo.py \ --server_ip $VLLM_NODE & # Run vLLM server on the 5th node (Group 2) srun --nodes=1 --ntasks=1 --nodelist="${NODELIST[4]}" trl vllm-serve --model Qwen/Qwen2.5-72B --tensor_parallel_size 8 & wait ``` ```python import argparse from datasets import load_dataset from trl import RLOOTrainer, RLOOConfig def main(): parser = argparse.ArgumentParser() parser.add_argument("--vllm_server_host", type=str, default="", help="The server IP") args = parser.parse_args() # Example dataset from TLDR dataset = load_dataset("trl-lib/tldr", split="train") # Dummy reward function: count the number of unique characters in the completions def reward_num_unique_chars(completions, **kwargs): return [len(set(c)) for c in completions] training_args = RLOOConfig( output_dir="Qwen2.5-72B-RLOO", per_device_train_batch_size=4, bf16=True, use_vllm=True, vllm_mode="server", vllm_server_host=args.vllm_server_host.replace("ip-", "").replace("-", "."), # from ip-X-X-X-X to X.X.X.X ) trainer = RLOOTrainer(model="Qwen/Qwen2.5-72B", args=training_args, reward_funcs=reward_num_unique_chars, train_dataset=dataset) trainer.train() if __name__=="__main__": main() ``` ### Using a custom reward function The [`RLOOTrainer`] supports using custom reward functions instead of dense reward models. To ensure compatibility, your reward function must satisfy the following requirements: Reward functions can be either synchronous Python callables or asynchronous `async def` coroutines. When you provide multiple asynchronous reward functions, they are awaited concurrently (run in parallel via `asyncio.gather`) so their latency overlaps. 1. **Input arguments**: - The function must accept the following as keyword arguments: - `prompts` (contains the prompts), - `completions` (contains the generated completions), - `completion_ids` (contains the tokenized completions), - `trainer_state` ([`~transformers.TrainerState`]): The current state of the trainer. This can be used to implement dynamic reward functions, such as curriculum learning, where the reward is adjusted based on the training progress. - `log_extra`: a callable `log_extra(column: str, values: list)` to add extra columns to the completions table. See Example 6. In distributed training, it's important that all processes log the same set of keys. - `log_metric`: a callable `log_metric(name: str, value: float)` to log scalar metrics as plots alongside `kl`, `entropy`, etc. See Example 6. In distributed training, it's important that all processes log the same set of keys. - All column names (but `prompt`) that the dataset may have. For example, if the dataset contains a column named `ground_truth`, the function will be called with `ground_truth` as a keyword argument. The easiest way to comply with this requirement is to use `**kwargs` in the function signature. - Depending on the dataset format, the input will vary: - For [standard format](dataset_formats#standard), `prompts` and `completions` will be lists of strings. - For [conversational format](dataset_formats#conversational), `prompts` and `completions` will be lists of message dictionaries. 2. **Return value**: The function must return a list of floats. Each float represents the reward corresponding to a single completion. #### Example 1: Reward longer completions Below is an example of a reward function for a standard format that rewards longer completions: ```python def reward_func(completion_ids, **kwargs): """Reward function that assigns higher scores to longer completions (in terms of token count).""" return [float(len(ids)) for ids in completion_ids] ``` You can test it as follows: ```python >>> prompts = ["The sky is", "The sun is"] # not used in the reward function, but the trainer will pass it >>> completions = [" blue.", " in the sky."] # not used in the reward function, but the trainer will pass it >>> completion_ids = [[6303, 13], [304, 279, 12884, 13]] >>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids) [2.0, 4.0] ``` #### Example 1.1: Reward longer completions (based on the number of characters) Same as the previous example, but this time the reward function is based on the number of characters instead of tokens. ```python def reward_func(completions, **kwargs): """Reward function that assigns higher scores to longer completions (in terms of character count).""" return [float(len(completion)) for completion in completions] ``` You can test it as follows: ```python >>> prompts = ["The sky is", "The sun is"] >>> completions = [" blue.", " in the sky."] >>> completion_ids = [[6303, 13], [304, 279, 12884, 13]] # not used in the reward function, but the trainer will pass it >>> reward_func(prompts=prompts, completions=completions, completion_ids=completion_ids) [6.0, 12.0] ``` #### Example 2: Reward completions with a specific format Below is an example of a reward function that checks if the completion has a specific format. This example is inspired by the _format reward_ function used in the paper [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://huggingface.co/papers/2501.12948). It is designed for a conversational format, where prompts and completions consist of structured messages. ```python import re def format_reward_func(completions, **kwargs): """Reward function that checks if the completion has a specific format.""" pattern = r"^.*?.*?$" completion_contents = [completion[0]["content"] for completion in completions] matches = [re.match(pattern, content) for content in completion_contents] return [1.0 if match else 0.0 for match in matches] ``` You can test this function as follows: ```python >>> prompts = [ ... [{"role": "assistant", "content": "What is the result of (1 + 2) * 4?"}], ... [{"role": "assistant", "content": "What is the result of (3 + 1) * 2?"}], ... ] >>> completions = [ ... [{"role": "assistant", "content": "The sum of 1 and 2 is 3, which we multiply by 4 to get 12.(1 + 2) * 4 = 12"}], ... [{"role": "assistant", "content": "The sum of 3 and 1 is 4, which we multiply by 2 to get 8. So (3 + 1) * 2 = 8."}], ... ] >>> format_reward_func(prompts=prompts, completions=completions) [1.0, 0.0] ``` #### Example 3: Reward completions based on a reference Below is an example of a reward function that checks if the completion is correct. This example is inspired by the _accuracy reward_ function used in the paper [DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning](https://huggingface.co/papers/2501.12948). This example is designed for [standard format](dataset_formats#standard), where the dataset contains a column named `ground_truth`. ```python import re def reward_func(completions, ground_truth, **kwargs): # Regular expression to capture content inside \boxed{} matches = [re.search(r"\\boxed\{(.*?)\}", completion) for completion in completions] contents = [match.group(1) if match else "" for match in matches] # Reward 1 if the content is the same as the ground truth, 0 otherwise return [1.0 if c == gt else 0.0 for c, gt in zip(contents, ground_truth)] ``` You can test this function as follows: ```python >>> prompts = ["Problem: Solve the equation $2x + 3 = 7$. Solution:", "Problem: Solve the equation $3x - 5 = 10$."] >>> completions = [r" The solution is \boxed{2}.", r" The solution is \boxed{6}."] >>> ground_truth = ["2", "5"] >>> reward_func(prompts=prompts, completions=completions, ground_truth=ground_truth) [1.0, 0.0] ``` #### Example 4: Multi-task reward functions Below is an example of using multiple reward functions in the [`RLOOTrainer`]. In this example, we define two task-specific reward functions: `math_reward_func` and `coding_reward_func`. The `math_reward_func` rewards math problems based on their correctness, while the `coding_reward_func` rewards coding problems based on whether the solution works. ```python from datasets import Dataset from trl import RLOOTrainer # Define a dataset that contains both math and coding problems dataset = Dataset.from_list( [ {"prompt": "What is 2+2?", "task": "math"}, {"prompt": "Write a function that returns the sum of two numbers.", "task": "code"}, {"prompt": "What is 3*4?", "task": "math"}, {"prompt": "Write a function that returns the product of two numbers.", "task": "code"}, ] ) # Math-specific reward function def math_reward_func(prompts, completions, task, **kwargs): rewards = [] for prompt, completion, t in zip(prompts, completions, task): if t == "math": # Calculate math-specific reward correct = check_math_solution(prompt, completion) reward = 1.0 if correct else -1.0 rewards.append(reward) else: # Return None for non-math tasks rewards.append(None) return rewards # Coding-specific reward function def coding_reward_func(prompts, completions, task, **kwargs): rewards = [] for prompt, completion, t in zip(prompts, completions, task): if t == "coding": # Calculate coding-specific reward works = test_code_solution(prompt, completion) reward = 1.0 if works else -1.0 rewards.append(reward) else: # Return None for non-coding tasks rewards.append(None) return rewards # Use both task-specific reward functions trainer = RLOOTrainer( model="Qwen/Qwen2-0.5B-Instruct", reward_funcs=[math_reward_func, coding_reward_func], train_dataset=dataset, ) trainer.train() ``` In this example, the `math_reward_func` and `coding_reward_func` are designed to work with a mixed dataset that contains both math and coding problems. The `task` column in the dataset is used to determine which reward function to apply to each problem. If there is no relevant reward function for a sample in the dataset, the reward function will return `None`, and the [`RLOOTrainer`] will continue with the valid functions and tasks. This allows the [`RLOOTrainer`] to handle multiple reward functions with different applicability. Note that the [`RLOOTrainer`] will ignore the `None` rewards returned by the reward functions and only consider the rewards returned by the relevant functions. This ensures that the model is trained on the relevant tasks and ignores the tasks for which there is no relevant reward function. #### Example 5: Asynchronous reward functions Custom reward functions can also be defined as `async def` coroutines. This is useful if your reward depends on slow I/O (for example, calling a remote service). When you pass multiple async reward functions, [`RLOOTrainer`] executes them concurrently so their latency overlaps. Below is a minimal example of an async reward function that simulates an I/O-bound operation: ```python import asyncio async def async_reward_func(prompts, completions, **kwargs): # Simulate an I/O-bound call (e.g., HTTP request, database lookup) await asyncio.sleep(0.01) # Simple toy reward: 1.0 if the completion is non-empty, else 0.0 return [1.0 if completion else 0.0 for completion in completions] ``` #### Example 6: Logging extra columns and metrics Below is an example of a reward function that logs extra columns to the completions table and scalar metrics as plots. ```python import re def reward_func(completions, ground_truth, log_extra=None, log_metric=None, **kwargs): extracted = [re.search(r"\\boxed\{(.*?)\}", c) for c in completions] extracted = [m.group(1) if m else None for m in extracted] rewards = [1.0 if e == gt else 0.0 for e, gt in zip(extracted, ground_truth)] if log_extra: log_extra("golden_answer", list(ground_truth)) log_extra("extracted_answer", [e or "[none]" for e in extracted]) if log_metric: log_metric("accuracy", sum(rewards) / len(rewards)) return rewards ``` #### Passing the reward function to the trainer To use your custom reward function, pass it to the [`RLOOTrainer`] as follows: ```python from trl import RLOOTrainer trainer = RLOOTrainer( reward_funcs=reward_func, ..., ) ``` You can pass several reward functions as a list; this list may include both synchronous and asynchronous functions: ```python from trl import RLOOTrainer trainer = RLOOTrainer( reward_funcs=[reward_func, async_reward_func1, async_reward_func2], ..., ) ``` and the reward will be computed as the sum of the rewards from each function, or the weighted sum if `reward_weights` is provided in the config. Note that [`RLOOTrainer`] supports multiple reward functions of different types. See the parameters documentation for more details. ## Vision-Language Model (VLM) Training RLOO supports training Vision-Language Models (VLMs) on multimodal datasets containing both text and images. ### Supported Models Tested with: - **Gemma3** — e.g., `google/gemma-3-4b-it` - **LLaVA-NeXT** — e.g., `llava-hf/llava-v1.6-mistral-7b-hf` - **Qwen2-VL** — e.g., `Qwen/Qwen2-VL-2B-Instruct` - **Qwen2.5-VL** — e.g., `Qwen/Qwen2.5-VL-3B-Instruct` - **SmolVLM2** — e.g., `HuggingFaceTB/SmolVLM2-2.2B-Instruct` > [!TIP] > Compatibility with all VLMs is not guaranteed. If you believe a model should be supported, feel free to open an issue on GitHub — or better yet, submit a pull request with the required changes. ### Quick Start Use [rloo\_vlm.py](https://github.com/huggingface/trl/blob/main/examples/scripts/rloo_vlm.py) to fine-tune a VLM. Example command for training on [`lmms-lab/multimodal-open-r1-8k-verified`](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified): ```bash accelerate launch \ --config_file=examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/rloo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir rloo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_vllm \ --vllm_mode colocate \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions ``` ### Configuration Tips - Use LoRA on vision-language projection layers - Enable 4-bit quantization to reduce memory usage - VLMs are memory-intensive — start with smaller batch sizes - Most models are compatible with vLLM (`server` and `colocate` modes) ### Dataset Format Each training sample should include: - `prompt`: Text formatted via the processor's chat template - `image`/`images`: PIL Image or list of PIL Images The trainer automatically handles image-to-tensor conversion via the model’s image processor. ## RLOOTrainer [[autodoc]] RLOOTrainer - train - save_model - push_to_hub ## RLOOConfig [[autodoc]] RLOOConfig ## References 1. [RLOO Paper](https://openreview.net/pdf?id=r1lgTGL5DE) 2. [Paper Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs](https://huggingface.co/papers/2402.14740) 3. [Paper - REINFORCE++: A Simple and Efficient Approach for Aligning Large Language Models](https://huggingface.co/papers/2501.03262) 4. [Blog Post - Putting RL back in RLHF](https://huggingface.co/blog/putting_rl_back_in_rlhf_with_rloo) 5. [Blog Post - Unraveling RLHF and Its Variants: Progress and Practical Engineering Insights](https://hijkzzz.notion.site/unraveling-rlhf-and-its-variants-engineering-insights#147d9a33ecc9806090f3d5c749d31f05) 6. [Youtube - RLOO: A Cost-Efficient Optimization for Learning from Human Feedback in LLMs](https://www.youtube.com/watch?v=86asXGPK6RU&ab_channel=BuzzRobot) ## Migration Guide from the old implementation (0.21 and below) With the release of version 0.22.0, we have revamped the [`RLOOTrainer`] to be more aligned with other online trainers in the library, like [`GRPOTrainer`]. This new implementation introduces several changes to the configuration parameters and overall structure of the trainer. Below is a summary of the key changes for [`RLOOConfig`]: | TRL ≤ 0.21.x | TRL ≥ 0.22.0 | | --- | --- | | `rloo_k` | renamed to `num_generations` | | `cliprange` | renamed to `epsilon` | | `kl_coef` | renamed to `beta` | | `exp_name` | renamed to `run_name`. Use `run_name = f"{exp_name}__{seed}__{int(time.time())}"` to replicate old behavior | | `normalize_reward` | renamed to `normalize_advantages`. Note: this always normalized advantages (despite the old name) | | `num_ppo_epochs` | renamed to `num_iterations` (default: `1`) | | `token_level_kl` | **removed** – KL is now computed only at the sequence level | | `dataset_num_proc` | **removed** – it was unused | | `num_mini_batches` | renamed to `steps_per_generation` | | `total_episodes` | use `max_steps=total_episodes / gradient_accumulation_steps` instead | | `local_rollout_forward_batch_size` | **removed** – now automatically set to `per_device_train_batch_size` (or `per_device_eval_batch_size` during evaluation) | | `num_sample_generations` | **removed** – use `logging_steps` to control generation logging frequency | | `response_length` | renamed to `max_completion_length` (default: `256`) | | `stop_token` | **removed** | | `stop_token_id` | **removed** – use `processing_class.eos_token_id` instead | | `missing_eos_penalty` | **removed** – replicate with a custom reward function checking if `eos_token_id` is in `completion_ids` | Below is a summary of the key changes for [`RLOOTrainer`]: | TRL ≤ 0.21.x | TRL ≥ 0.22.0 | | --- | --- | | `config` | renamed to `args` | | `reward_model` | renamed to `reward_funcs`, which now supports both reward models and custom reward functions | | `policy` | renamed to `model` | | `ref_policy` | **removed** – the reference model is now created automatically from `model` | | `data_collator` | **removed** | ================================================ FILE: docs/source/script_utils.md ================================================ # Scripts Utilities ## ScriptArguments [[autodoc]] ScriptArguments ## TrlParser [[autodoc]] TrlParser - parse_args_and_config - parse_args_into_dataclasses - set_defaults_with_config ## get_dataset [[autodoc]] get_dataset ## DatasetConfig [[autodoc]] scripts.utils.DatasetConfig ## DatasetMixtureConfig [[autodoc]] DatasetMixtureConfig ================================================ FILE: docs/source/sft_trainer.md ================================================ # SFT Trainer [![All_models-SFT-blue](https://img.shields.io/badge/All_models-SFT-blue)](https://huggingface.co/models?other=sft,trl) [![smol_course-Chapter_1-yellow](https://img.shields.io/badge/smol_course-Chapter_1-yellow)](https://github.com/huggingface/smol-course/tree/main/1_instruction_tuning) ## Overview TRL supports the Supervised Fine-Tuning (SFT) Trainer for training language models. This post-training method was contributed by [Younes Belkada](https://huggingface.co/ybelkada). ## Quick start This example demonstrates how to train a language model using the [`SFTTrainer`] from TRL. We train a [Qwen 3 0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) model on the [Capybara dataset](https://huggingface.co/datasets/trl-lib/Capybara), a compact, diverse multi-turn dataset to benchmark reasoning and generalization. ```python from trl import SFTTrainer from datasets import load_dataset trainer = SFTTrainer( model="Qwen/Qwen3-0.6B", train_dataset=load_dataset("trl-lib/Capybara", split="train"), ) trainer.train() ``` ## Expected dataset type and format SFT supports both [language modeling](dataset_formats#language-modeling) and [prompt-completion](dataset_formats#prompt-completion) datasets. The [`SFTTrainer`] is compatible with both [standard](dataset_formats#standard) and [conversational](dataset_formats#conversational) dataset formats. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ```python # Standard language modeling {"text": "The sky is blue."} # Conversational language modeling {"messages": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}]} # Standard prompt-completion {"prompt": "The sky is", "completion": " blue."} # Conversational prompt-completion {"prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}]} ``` If your dataset is not in one of these formats, you can preprocess it to convert it into the expected format. Here is an example with the [FreedomIntelligence/medical-o1-reasoning-SFT](https://huggingface.co/datasets/FreedomIntelligence/medical-o1-reasoning-SFT) dataset: ```python from datasets import load_dataset dataset = load_dataset("FreedomIntelligence/medical-o1-reasoning-SFT", "en") def preprocess_function(example): return { "prompt": [{"role": "user", "content": example["Question"]}], "completion": [ {"role": "assistant", "content": f"{example['Complex_CoT']}{example['Response']}"} ], } dataset = dataset.map(preprocess_function, remove_columns=["Question", "Response", "Complex_CoT"]) print(next(iter(dataset["train"]))) ``` ```json { "prompt": [ { "content": "Given the symptoms of sudden weakness in the left arm and leg, recent long-distance travel, and the presence of swollen and tender right lower leg, what specific cardiac abnormality is most likely to be found upon further evaluation that could explain these findings?", "role": "user", } ], "completion": [ { "content": "Okay, let's see what's going on here. We've got sudden weakness [...] clicks into place!The specific cardiac abnormality most likely to be found in [...] the presence of a PFO facilitating a paradoxical embolism.", "role": "assistant", } ], } ``` ## Looking deeper into the SFT method Supervised Fine-Tuning (SFT) is the simplest and most commonly used method to adapt a language model to a target dataset. The model is trained in a fully supervised fashion using pairs of input and output sequences. The goal is to minimize the negative log-likelihood (NLL) of the target sequence, conditioning on the input. This section breaks down how SFT works in practice, covering the key steps: **preprocessing**, **tokenization** and **loss computation**. ### Preprocessing and tokenization During training, each example is expected to contain a **text field** or a **(prompt, completion)** pair, depending on the dataset format. For more details on the expected formats, see [Dataset formats](dataset_formats). The [`SFTTrainer`] tokenizes each input using the model's tokenizer. If both prompt and completion are provided separately, they are concatenated before tokenization. ### Computing the loss ![sft_figure](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/sft_figure.png) The loss used in SFT is the **token-level cross-entropy loss**, defined as: $$ \mathcal{L}_{\text{SFT}}(\theta) = - \sum_{t=1}^{T} \log p_\theta(y_t \mid y_{ [!TIP] > The paper [On the Generalization of SFT: A Reinforcement Learning Perspective with Reward Rectification](https://huggingface.co/papers/2508.05629) proposes an alternative loss function, called **Dynamic Fine-Tuning (DFT)**, which aims to improve generalization by rectifying the reward signal. This method can be enabled by setting `loss_type="dft"` in the [`SFTConfig`]. For more details, see [Paper Index - Dynamic Fine-Tuning](paper_index#on-the-generalization-of-sft-a-reinforcement-learning-perspective-with-reward-rectification). ### Label shifting and masking During training, the loss is computed using a **one-token shift**: the model is trained to predict each token in the sequence based on all previous tokens. Specifically, the input sequence is shifted right by one position to form the target labels. Padding tokens (if present) are ignored in the loss computation by applying an ignore index (default: `-100`) to the corresponding positions. This ensures that the loss focuses only on meaningful, non-padding tokens. ## Logged metrics While training and evaluating we record the following reward metrics: * `global_step`: The total number of optimizer steps taken so far. * `epoch`: The current epoch number, based on dataset iteration. * `num_tokens`: The total number of tokens processed so far. * `loss`: The average cross-entropy loss computed over non-masked tokens in the current logging interval. * `entropy`: The average entropy of the model's predicted token distribution over non-masked tokens. * `mean_token_accuracy`: The proportion of non-masked tokens for which the model’s top-1 prediction matches the ground truth token. * `learning_rate`: The current learning rate, which may change dynamically if a scheduler is used. * `grad_norm`: The L2 norm of the gradients, computed before gradient clipping. ## Customization ### Model initialization You can directly pass the kwargs of the [`~transformers.AutoModelForCausalLM.from_pretrained()`] method to the [`SFTConfig`]. For example, if you want to load a model in a different precision, analogous to ```python model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B", dtype=torch.bfloat16) ``` you can do so by passing the `model_init_kwargs={"dtype": torch.bfloat16}` argument to the [`SFTConfig`]. ```python from trl import SFTConfig training_args = SFTConfig( model_init_kwargs={"dtype": torch.bfloat16}, ) ``` Note that all keyword arguments of [`~transformers.AutoModelForCausalLM.from_pretrained()`] are supported. ### Packing [`SFTTrainer`] supports _example packing_, where multiple examples are packed in the same input sequence to increase training efficiency. To enable packing, simply pass `packing=True` to the [`SFTConfig`] constructor. ```python training_args = SFTConfig(packing=True) ``` For more details on packing, see [Packing](reducing_memory_usage#packing). ### Train on assistant messages only To train on assistant messages only, use a [conversational](dataset_formats#conversational) dataset and set `assistant_only_loss=True` in the [`SFTConfig`]. This setting ensures that loss is computed **only** on the assistant responses, ignoring user or system messages. ```python training_args = SFTConfig(assistant_only_loss=True) ``` ![train_on_assistant](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/train_on_assistant.png) > [!WARNING] > This functionality is only available for chat templates that support returning the assistant tokens mask via the `{% generation %}` and `{% endgeneration %}` keywords. For an example of such a template, see [HugggingFaceTB/SmolLM3-3B](https://huggingface.co/HuggingFaceTB/SmolLM3-3B/blob/main/chat_template.jinja#L76-L82). ### Train on completion only To train on completion only, use a [prompt-completion](dataset_formats#prompt-completion) dataset. By default, the trainer computes the loss on the completion tokens only, ignoring the prompt tokens. If you want to train on the full sequence, set `completion_only_loss=False` in the [`SFTConfig`]. ![train_on_completion](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/train_on_completion.png) > [!TIP] > Training on completion only is compatible with training on assistant messages only. In this case, use a [conversational](dataset_formats#conversational) [prompt-completion](dataset_formats#prompt-completion) dataset and set `assistant_only_loss=True` in the [`SFTConfig`]. ### Train adapters with PEFT We support tight integration with 🤗 PEFT library, allowing any user to conveniently train adapters and share them on the Hub, rather than training the entire model. ```python from datasets import load_dataset from trl import SFTTrainer from peft import LoraConfig dataset = load_dataset("trl-lib/Capybara", split="train") trainer = SFTTrainer( "Qwen/Qwen3-0.6B", train_dataset=dataset, peft_config=LoraConfig(), ) trainer.train() ``` You can also continue training your [`~peft.PeftModel`]. For that, first load a `PeftModel` outside [`SFTTrainer`] and pass it directly to the trainer without the `peft_config` argument being passed. ```python from datasets import load_dataset from trl import SFTTrainer from peft import AutoPeftModelForCausalLM model = AutoPeftModelForCausalLM.from_pretrained("trl-lib/Qwen3-4B-LoRA", is_trainable=True) dataset = load_dataset("trl-lib/Capybara", split="train") trainer = SFTTrainer( model=model, train_dataset=dataset, ) trainer.train() ``` > [!TIP] > When training adapters, you typically use a higher learning rate (≈1e‑4) since only new parameters are being learned. > > ```python > SFTConfig(learning_rate=1e-4, ...) > ``` ### Train with Liger Kernel Liger Kernel is a collection of Triton kernels for LLM training that boosts multi-GPU throughput by 20%, cuts memory use by 60% (enabling up to 4× longer context), and works seamlessly with tools like FlashAttention, PyTorch FSDP, and DeepSpeed. For more information, see [Liger Kernel Integration](liger_kernel_integration). ### Rapid Experimentation for SFT RapidFire AI is an open-source experimentation engine that sits on top of TRL and lets you launch multiple SFT configurations at once, even on a single GPU. Instead of trying configurations sequentially, RapidFire lets you **see all their learning curves earlier, stop underperforming runs, and clone promising ones with new settings in flight** without restarting. For more information, see [RapidFire AI Integration](rapidfire_integration). ### Train with Unsloth Unsloth is an open‑source framework for fine‑tuning and reinforcement learning that trains LLMs (like Llama, Mistral, Gemma, DeepSeek, and more) up to 2× faster with up to 70% less VRAM, while providing a streamlined, Hugging Face–compatible workflow for training, evaluation, and deployment. For more information, see [Unsloth Integration](unsloth_integration). ## Instruction tuning example **Instruction tuning** teaches a base language model to follow user instructions and engage in conversations. This requires: 1. **Chat template**: Defines how to structure conversations into text sequences, including role markers (user/assistant), special tokens, and turn boundaries. Read more about chat templates in [Chat templates](https://huggingface.co/docs/transformers/chat_templating#templates). 2. **Conversational dataset**: Contains instruction-response pairs This example shows how to transform the [Qwen 3 0.6B Base](https://huggingface.co/Qwen/Qwen3-0.6B-Base) model into an instruction-following model using the [Capybara dataset](https://huggingface.co/datasets/trl-lib/Capybara) and a chat template from [HuggingFaceTB/SmolLM3-3B](https://huggingface.co/HuggingFaceTB/SmolLM3-3B). The SFT Trainer automatically handles tokenizer updates and special token configuration. ```python from trl import SFTConfig, SFTTrainer from datasets import load_dataset trainer = SFTTrainer( model="Qwen/Qwen3-0.6B-Base", args=SFTConfig( output_dir="Qwen3-0.6B-Instruct", chat_template_path="HuggingFaceTB/SmolLM3-3B", ), train_dataset=load_dataset("trl-lib/Capybara", split="train"), ) trainer.train() ``` > [!WARNING] > Some base models, like those from Qwen, have a predefined chat template in the model's tokenizer. In these cases, it is not necessary to apply [`clone_chat_template()`], as the tokenizer already handles the formatting. However, it is necessary to align the EOS token with the chat template to ensure the model's responses terminate correctly. In these cases, specify `eos_token` in [`SFTConfig`]; for example, for `Qwen/Qwen2.5-1.5B`, one should set `eos_token="<|im_end|>"`. Once trained, your model can now follow instructions and engage in conversations using its new chat template. ```python >>> from transformers import pipeline >>> pipe = pipeline("text-generation", model="Qwen3-0.6B-Instruct/checkpoint-5000") >>> prompt = "<|im_start|>user\nWhat is the capital of France? Answer in one word.<|im_end|>\n<|im_start|>assistant\n" >>> response = pipe(prompt) >>> response[0]["generated_text"] '<|im_start|>user\nWhat is the capital of France? Answer in one word.<|im_end|>\n<|im_start|>assistant\nThe capital of France is Paris.' ``` Alternatively, use the structured conversation format (recommended): ```python >>> prompt = [{"role": "user", "content": "What is the capital of France? Answer in one word."}] >>> response = pipe(prompt) >>> response[0]["generated_text"] [{'role': 'user', 'content': 'What is the capital of France? Answer in one word.'}, {'role': 'assistant', 'content': 'The capital of France is Paris.'}] ``` ## Tool Calling with SFT The [`SFTTrainer`] fully supports fine-tuning models with _tool calling_ capabilities. In this case, each dataset example should include: * The conversation messages, including any tool calls (`tool_calls`) and tool responses (`tool` role messages) * The list of available tools in the `tools` column, typically provided as JSON schemas For details on the expected dataset structure, see the [Dataset Format — Tool Calling](dataset_formats#tool-calling) section. ## Training Vision Language Models [`SFTTrainer`] fully supports training Vision-Language Models (VLMs). To train a VLM, provide a dataset with either an `image` column (single image per sample) or an `images` column (list of images per sample). For more information on the expected dataset structure, see the [Dataset Format — Vision Dataset](dataset_formats#vision-dataset) section. An example of such a dataset is the [LLaVA Instruct Mix](https://huggingface.co/datasets/trl-lib/llava-instruct-mix). ```python from trl import SFTConfig, SFTTrainer from datasets import load_dataset trainer = SFTTrainer( model="Qwen/Qwen2.5-VL-3B-Instruct", args=SFTConfig(max_length=None), train_dataset=load_dataset("trl-lib/llava-instruct-mix", split="train"), ) trainer.train() ``` > [!TIP] > For VLMs, truncating may remove image tokens, leading to errors during training. To avoid this, set `max_length=None` in the [`SFTConfig`]. This allows the model to process the full sequence length without truncating image tokens. > > ```python > SFTConfig(max_length=None, ...) > ``` > > Only use `max_length` when you've verified that truncation won't remove image tokens for the entire dataset. ## SFTTrainer [[autodoc]] SFTTrainer - train - save_model - push_to_hub ## SFTConfig [[autodoc]] SFTConfig ================================================ FILE: docs/source/speeding_up_training.md ================================================ # Speeding Up Training This guide covers various methods to accelerate training in TRL. Each technique includes minimal examples with links to more comprehensive documentation. ## vLLM for fast generation in online methods [Online methods](index#online-methods) such as GRPO or Online DPO require the model to generate completions, which is often a slow process and can significantly impact training time. To speed up generation, you can use [vLLM](https://github.com/vllm-project/vllm), a library that enables fast generation through, among other things, PagedAttention. TRL's online trainers support vLLM, greatly improving training speed. For more details, see [vLLM Integration](vllm_integration). To use [vLLM](https://github.com/vllm-project/vllm), first install it using: ```bash pip install trl[vllm] ``` First, start a vLLM server by running: ```bash trl vllm-serve --model ``` Then, run the training script and pass `use_vllm=True` in the training arguments. ```python from trl.experimental.online_dpo import OnlineDPOConfig training_args = OnlineDPOConfig(..., use_vllm=True, vllm_mode="server") ``` First, start a vLLM server by running: ```bash trl vllm-serve --model ``` Then, run the training script and pass `use_vllm=True` in the training arguments. ```python from trl import GRPOConfig training_args = GRPOConfig(..., use_vllm=True, vllm_mode="server") ``` You can customize the server configuration by passing additional arguments. For more information, see [vLLM integration](vllm_integration). > [!WARNING] > When using vLLM, ensure that the GPUs assigned for training and generation are separate to avoid resource conflicts. For instance, if you plan to use 4 GPUs for training and another 4 for vLLM generation, you can specify GPU allocation using `CUDA_VISIBLE_DEVICES`. > > Set GPUs **0-3** for vLLM generation: > > ```sh > CUDA_VISIBLE_DEVICES=0,1,2,3 trl vllm-serve --model > ``` > > And GPUs **4-7** for training: > > ```sh > CUDA_VISIBLE_DEVICES=4,5,6,7 accelerate launch train.py > ``` First, start a vLLM server by running: ```bash trl vllm-serve --model ``` Then, run the training script and pass `use_vllm=True` in the training arguments. ```python from trl import RLOOConfig training_args = RLOOConfig(..., use_vllm=True, vllm_mode="server") ``` You can customize the server configuration by passing additional arguments. For more information, see [vLLM integration](vllm_integration). > [!WARNING] > When using vLLM, ensure that the GPUs assigned for training and generation are separate to avoid resource conflicts. For instance, if you plan to use 4 GPUs for training and another 4 for vLLM generation, you can specify GPU allocation using `CUDA_VISIBLE_DEVICES`. > > Set GPUs **0-3** for vLLM generation: > > ```sh > CUDA_VISIBLE_DEVICES=0,1,2,3 trl vllm-serve --model > ``` > > And GPUs **4-7** for training: > > ```sh > CUDA_VISIBLE_DEVICES=4,5,6,7 accelerate launch train.py > ``` ## Optimized attention implementations TRL supports various optimized attention implementations that can significantly speed up training while reducing memory usage. You can use either a pre-optimized kernels directly from the [Kernels Hub](kernels_hub) or a manually built attention backend. You can use pre-optimized attention kernels from the Hub without manual compilation: ```python from trl import SFTConfig training_args = SFTConfig(..., model_init_kwargs={"attn_implementation": "kernels-community/flash-attn2"}) ``` Other options include `kernels-community/vllm-flash-attn3` and `kernels-community/paged-attention`. Optimized attention works across all TRL trainers. For more details, see [Kernels Hub Integration](kernels_hub). > [!WARNING] > Manually building optimized attention backends is complex and time-consuming. It's never recommended unless absolutely necessary. Consider using Kernels from the Hub instead, as described in the previous section. If you have manually installed an optimized attention backend like Flash Attention 2, you can specify it in the training arguments: ```python from trl import SFTConfig training_args = SFTConfig(..., model_init_kwargs={"attn_implementation": "flash_attention_2"}) ``` ## Liger Kernel for memory optimization Liger Kernel is a collection of Triton kernels designed for LLM training that can increase throughput by 20% and reduce memory usage by 60%. ```python from trl import SFTConfig training_args = SFTConfig(..., use_liger_kernel=True) ``` ```python from trl import DPOConfig training_args = DPOConfig(..., use_liger_kernel=True) ``` ```python from trl import GRPOConfig training_args = GRPOConfig(..., use_liger_kernel=True) ``` ```python from trl.experimental.kto import KTOConfig training_args = KTOConfig(..., use_liger_kernel=True) ``` ```python from trl.experimental.gkd import GKDConfig training_args = GKDConfig(..., use_liger_kernel=True) ``` For more information, see [Liger Kernel Integration](liger_kernel_integration). ## Mixed precision training Mixed precision training using bf16 or fp16 can speed up training and reduce memory usage with minimal impact on model quality. ```python from trl import SFTConfig training_args = SFTConfig(..., bf16=True) # or fp16=True for older GPUs ``` Use `bf16=True` for Ampere GPUs (A100, RTX 30xx) or newer, and `fp16=True` for older GPUs. Mixed precision training is supported across all TRL trainers. ================================================ FILE: docs/source/trackio_integration.md ================================================ # Trackio Integration [Trackio](https://huggingface.co/docs/trackio) is a lightweight, free experiment tracking library built on top of **🤗 Datasets** and **🤗 Spaces**. It is the **recommended tracking solution for TRL** and comes natively integrated with all trainers. To enable logging, simply set `report_to="trackio"` in your training config: ```python from trl import SFTConfig # works with any trainer config (e.g. DPOConfig, GRPOConfig, etc.) training_args = SFTConfig( ..., report_to="trackio", # enable Trackio logging ) ``` ## Organizing Your Experiments with Run Names and Projects By default, Trackio will generate a name to identify each run. However, we highly recommend setting a descriptive `run_name` to make it easier to organize experiments. For example: ```python from trl import SFTConfig training_args = SFTConfig( ..., report_to="trackio", run_name="sft_qwen3-4b_lr2e-5_bs128", # descriptive run name ) ``` You can also group related experiments by project by setting the following environment variable: ```bash export TRACKIO_PROJECT="my_project" ``` ## Hosting Your Logs on 🤗 Spaces Trackio has local-first design, meaning your logs stay on your machine. If you’d like to host them and deploy a dashboard on **🤗 Spaces**, set: ```bash export TRACKIO_SPACE_ID="username/space_id" ``` Running the following example: ```python import os from trl import SFTConfig, SFTTrainer from datasets import load_dataset os.environ["TRACKIO_SPACE_ID"] = "trl-lib/trackio" os.environ["TRACKIO_PROJECT"] = "trl-documentation" trainer = SFTTrainer( model="Qwen/Qwen3-0.6B", train_dataset=load_dataset("trl-lib/Capybara", split="train"), args=SFTConfig( report_to="trackio", run_name="sft_qwen3-0.6b_capybara", ), ) trainer.train() ``` will give you a hosted dashboard at https://huggingface.co/spaces/trl-lib/trackio. ================================================ FILE: docs/source/unsloth_integration.md ================================================ # Unsloth Integration Unsloth is an open‑source framework for fine‑tuning and reinforcement learning that trains LLMs (like Llama, OpenAI gpt-oss, Mistral, Gemma, DeepSeek, and more) up to 2× faster with up to 80% less VRAM. Unsloth allows [training](https://huggingface.co/docs/trl/en/unsloth_integration#Training), evaluation, running and [deployment](https://huggingface.co/docs/trl/en/unsloth_integration#Saving-the-model) with other inference engines like llama.cpp, Ollama and vLLM. The library provides a streamlined, Hugging Face compatible workflow for training, evaluation, inference and deployment and is fully compatible with [`SFTTrainer`]. ## Key Features - Training support for all transformer compatible models: Text-to-speech (TTS), multimodal, BERT, RL and more - Supports full fine-tuning, pretraining, LoRA, QLoRA, 8-bit training & more - Works on Linux, Windows, Colab, Kaggle; NVIDIA GPUs, soon AMD & Intel setups - Supports most features TRL supports, including RLHF (GSPO, GRPO, DPO etc.) - Hand-written Triton kernels and a manual backprop engine ensure no accuracy degradation (0% approximation error) ## Installation ### pip install Local Installation (Linux recommended): ```sh pip install unsloth ``` You can also install `unsloth` according to the [official documentation](https://docs.unsloth.ai/get-started/installing-+-updating). Once installed, you can incorporate unsloth into your workflow in a very simple manner; instead of loading [`~transformers.AutoModelForCausalLM`], you just need to load a `FastLanguageModel` as follows: ```python import torch from trl import SFTConfig, SFTTrainer from unsloth import FastLanguageModel max_length = 2048 # Supports automatic RoPE Scaling, so choose any number # Load model model, tokenizer = FastLanguageModel.from_pretrained( model_name="unsloth/mistral-7b", max_seq_length=max_length, dtype="auto", # For auto-detection. Float16 for Tesla T4, V100, Bfloat16 for Ampere+ load_in_4bit=True, # Use 4bit quantization to reduce memory usage. Can be False ) # Do model patching and add fast LoRA weights model = FastLanguageModel.get_peft_model( model, r=16, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], lora_alpha=16, lora_dropout=0, # Dropout = 0 is currently optimized bias="none", # Bias = "none" is currently optimized use_gradient_checkpointing=True, random_state=3407, ) training_args = SFTConfig(output_dir="./output", max_length=max_length) trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset, ) trainer.train() ``` The saved model is fully compatible with Hugging Face's transformers library. Learn more about unsloth in their [official repository](https://github.com/unslothai/unsloth). ### Docker Install ```sh docker run -d -e JUPYTER_PASSWORD="mypassword" \ -p 8888:8888 -p 2222:22 \ -v $(pwd)/work:/workspace/work \ --gpus all \ unsloth/unsloth ``` Access Jupyter Lab at ```http://localhost:8888``` and start fine-tuning! ## Training These are some core settings you can toggle before training: - ```max_seq_length = 2048``` – Controls context length. While Llama-3 supports 8192, we recommend 2048 for testing. Unsloth enables 4× longer context fine-tuning. - ```dtype = "auto"``` – For auto-detection; use torch.float16 or torch.bfloat16 for newer GPUs. - ```load_in_4bit = True``` – Enables 4-bit quantization, reducing memory use 4× for fine-tuning. Disabling it allows for LoRA 16-bit fine-tuning to be enabled. - To enable full fine-tuning (FFT), set ```full_finetuning = True```. For 8-bit fine-tuning, set ```load_in_8bit = True```. Note: Only one training method can be set to True at a time. For more information on configuring Unsloth's hyperparameters and features, read their [documentation guide here](https://docs.unsloth.ai/get-started/fine-tuning-llms-guide). ## Saving the model Unsloth allows you to directly save the finetuned model as a small file called a LoRA adapter. You can instead push to the Hugging Face hub as well if you want to upload your model! Remember to get a [Hugging Face token](https://huggingface.co/settings/tokens) and add your token! ### Saving to GGUF To save to GGUF, Unsloth uses llama.cpp. To save locally: ```python model.save_pretrained_gguf("directory", tokenizer, quantization_method = "q4_k_m") model.save_pretrained_gguf("directory", tokenizer, quantization_method = "q8_0") model.save_pretrained_gguf("directory", tokenizer, quantization_method = "f16") ``` To push to the hub: ```python model.push_to_hub_gguf("hf_username/directory", tokenizer, quantization_method = "q4_k_m") model.push_to_hub_gguf("hf_username/directory", tokenizer, quantization_method = "q8_0") ``` ### Saving to vLLM To save to 16-bit for vLLM, use: ```python model.save_pretrained_merged("model", tokenizer, save_method = "merged_16bit",) model.push_to_hub_merged("hf/model", tokenizer, save_method = "merged_16bit", token = "") ``` ================================================ FILE: docs/source/use_model.md ================================================ # Use model after training Once you have trained a model using either the SFTTrainer, PPOTrainer, or DPOTrainer, you will have a fine-tuned model that can be used for text generation. In this section, we'll walk through the process of loading the fine-tuned model and generating text. If you need to run an inference server with the trained model, you can explore libraries such as [`text-generation-inference`](https://github.com/huggingface/text-generation-inference). ## Load and Generate If you have fine-tuned a model fully, meaning without the use of PEFT you can simply load it like any other language model in transformers. E.g. the value head that was trained during the PPO training is no longer needed and if you load the model with the original transformer class it will be ignored: ```python from transformers import AutoTokenizer, AutoModelForCausalLM model_name_or_path = "Qwen/Qwen3-0.6B" #path/to/your/model/or/name/on/hub device = "cpu" # or "cuda" if you have a GPU model = AutoModelForCausalLM.from_pretrained(model_name_or_path).to(device) tokenizer = AutoTokenizer.from_pretrained(model_name_or_path) inputs = tokenizer.encode("This movie was really", return_tensors="pt").to(device) outputs = model.generate(inputs) print(tokenizer.decode(outputs[0])) ``` Alternatively you can also use the pipeline: ```python from transformers import pipeline model_name_or_path = "Qwen/Qwen3-0.6B" #path/to/your/model/or/name/on/hub pipe = pipeline("text-generation", model=model_name_or_path) print(pipe("This movie was really")[0]["generated_text"]) ``` ## Use Adapters PEFT ```python from peft import PeftConfig, PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model_name = "Qwen/Qwen3-0.6B" #path/to/your/model/or/name/on/hub adapter_model_name = "path/to/my/adapter" model = AutoModelForCausalLM.from_pretrained(base_model_name) model = PeftModel.from_pretrained(model, adapter_model_name) tokenizer = AutoTokenizer.from_pretrained(base_model_name) ``` You can also merge the adapters into the base model so you can use the model like a normal transformers model, however the checkpoint will be significantly bigger: ```python model = AutoModelForCausalLM.from_pretrained(base_model_name) model = PeftModel.from_pretrained(model, adapter_model_name) model = model.merge_and_unload() model.save_pretrained("merged_adapters") ``` Once you have the model loaded and either merged the adapters or keep them separately on top you can run generation as with a normal model outlined above. ================================================ FILE: docs/source/vllm_integration.md ================================================ # vLLM Integration This document will guide you through the process of using vLLM with TRL for faster generation in online methods like GRPO and Online DPO. We first summarize a tl;dr on how to use vLLM with TRL, and then we will go into the details of how it works under the hood. > [!WARNING] > TRL currently only supports vLLM versions from `0.10.2` to `0.17.1`. Please ensure you have a version in this range installed to avoid compatibility issues. > [!TIP] > The following trainers currently support generation with vLLM: > > - [`GRPOTrainer`] > - [`RLOOTrainer`] > - [`experimental.nash_md.NashMDTrainer`] > - [`experimental.online_dpo.OnlineDPOTrainer`] > - [`experimental.xpo.XPOTrainer`] ## 🚀 How can I use vLLM with TRL to speed up training? 💡 **Note**: Resources required for this specific example: a single node with 8 GPUs. > [!WARNING] > When using vLLM with TRL, the **vLLM server** and the **trainer** must run on **separate CUDA devices** to prevent conflicts. > For guidance on configuring this properly, see [Modes of using vLLM during training](#modes-of-using-vllm-during-training). First, install vLLM using the following command: ```bash pip install "trl[vllm]" ``` Then run the server on specific GPUs (e.g., GPUs 0-3): ```sh CUDA_VISIBLE_DEVICES=0,1,2,3 trl vllm-serve --model Qwen/Qwen2.5-7B --tensor-parallel-size 4 ``` Once the server is running, you can use it to generate completions for training. In the example below, we are using the different supported trainers using the vLLM server for generation. The `--tensor-parallel-size` and `--data-parallel-size` arguments control how the model and data are sharded across GPUs. In this example, we shard one model across 4 GPUs with tensor parallelism. Then, run the training script on different GPUs (e.g., GPUs 4-7) by passing `use_vllm=True` in the training arguments as follows: Sample of a simple `train.py` script: ```python from datasets import load_dataset from trl import GRPOTrainer, GRPOConfig from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = GRPOTrainer( model="Qwen/Qwen2.5-7B", args=GRPOConfig(use_vllm=True, vllm_mode="server"), reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` ```python from datasets import load_dataset from trl.experimental.online_dpo import OnlineDPOConfig, OnlineDPOTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = OnlineDPOTrainer( model="Qwen/Qwen2.5-7B", args=OnlineDPOConfig(use_vllm=True, vllm_mode="server"), reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` ```python from datasets import load_dataset from trl.experimental.nash_md import NashMDConfig, NashMDTrainer from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = NashMDTrainer( model="Qwen/Qwen2.5-7B", args=NashMDConfig(use_vllm=True, vllm_mode="server"), reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` ```python from datasets import load_dataset from trl.experimental.xpo import XPOTrainer, XPOConfig from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = XPOTrainer( model="Qwen/Qwen2.5-7B", args=XPOConfig(use_vllm=True, vllm_mode="server"), reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` ```python from datasets import load_dataset from trl import RLOOTrainer, RLOOConfig from trl.rewards import accuracy_reward dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = RLOOTrainer( model="Qwen/Qwen2.5-7B", args=RLOOConfig(use_vllm=True, vllm_mode="server"), reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` And the train command on separate GPUs from the server: ```sh CUDA_VISIBLE_DEVICES=4,5,6,7 accelerate launch train.py ``` ## Why using vLLM? ### 🎬 Flashback: Why do we need to use vLLM in online methods? Online methods like GRPO or Online DPO require the model to generate completions during training, which are then used to compute reward signals. However, generation can be extremely time-consuming, especially with large or reasoning models. In the default setup (without vLLM), completions are generated using the [(unwrapped) model's `generate` method](https://github.com/huggingface/trl/blob/f3e8c2304428ef16e9ae5de9e5741ed84d533b7b/trl/trainer/grpo_trainer.py#L965C39-L965C66). This approach quickly becomes a major bottleneck — generation is slow and inefficient, particularly for large batches or models. As a result, training times increase significantly, and overall efficiency drops. To address this, we turn to vLLM, which enables much faster and more scalable generation, helping eliminate this bottleneck in online methods. ### 🤔 How does vLLM solve the slow generation issue? If you've ever done autoregressive decoder training, you know all the input tokens to the LLM produce their attention key and value tensors, and these tensors are kept in GPU memory to later generate subsequent tokens based on them. These cached key and value tensors are often referred to as the KV cache. However, storing the KV cache occupies a lot of memory, so vLLM uses a technique called **PagedAttention** to solve this problem. PagedAttention, which is inspired by the OS’s virtual memory concept, stores continuous keys and values in **non-contiguous memory space**, which is much more efficient. The details of this are beyond the scope of this document, but in short, it allows the model to store the keys and values in a more efficient way, reducing the memory footprint and speeding up the generation process. If you are interested, make sure to check out the [vLLM PagedAttention](https://blog.vllm.ai/2023/06/20/vllm.html) for more details. ## How vLLM Works (Under the Hood) 🔍 ### 🤔 What exactly happens when you run `trl vllm-serve --model `? When you run for example ```sh CUDA_VISIBLE_DEVICES=0,1,2,3 trl vllm-serve --model Qwen/Qwen2.5-7B --tensor-parallel-size 4 ``` 1. vLLM first spawns multiple workers to handle incoming requests in parallel. The number of workers is determined by multiplying the `--tensor-parallel-size` and `--data-parallel-size` values. In this example, it spawns 4 workers (4 × 1). Each worker operates independently and processes a chunk of the incoming requests — which are basically the prompts sent to the server for generation. 2. Once the incoming requests (prompts) are distributed across the workers, the model starts generating completions. Internally, the model’s weights are split across multiple GPUs based on the `--tensor-parallel-size` argument — this is how tensor parallelism is handled. 3. Although the GPUs process requests independently and in parallel, they still need to communicate with each other. Remember that each GPU handles only a slice of the incoming prompts (for example, with 4 GPUs and 8 prompts using `--tensor-parallel-size=4`, each GPU participates in serving the full model). This GPU-to-GPU communication is managed efficiently by NVIDIA’s NCCL library. The communication mainly ensures that each GPU gets its correct portion of the incoming requests — it’s lightweight and doesn’t interfere with generation itself. Separately, the number of completions to generate per prompt is controlled by the `num_generations` setting in the GRPO config. For instance, if you set `num_generations=2` (like in the picture above), each prompt will have 2 completions. So, with 8 prompts and `num_generations=2`, you would end up with 16 completions total — regardless of the number of GPUs or parallelism settings. ### 🥸 More detail on what happens under the hood when running the server - The vLLM server starts by running the command: `trl vllm-serve --model Qwen/Qwen2.5-7B`. - Once the server is running, it generates completions based on requests from the client (trainer) using `vllm_client.generate` [these lines](https://github.com/huggingface/trl/blob/cc044e35b285be7dc062764b3364e1e684db4c7c/trl/trainer/grpo_trainer.py#L1025-L1035). - The client (trainer) then requests these completions from the server. - These completions are used to compute the reward signal. - Based on the reward signal and the model’s output, the loss is computed, and the backward pass is performed to update the model’s weights. - **Note**: The server only handles completion generation — it doesn’t train the model. Therefore, the model’s weights aren’t updated on the server. Once the backward pass is complete, the client sends the updated weights to the server using `vllm_client.update_named_param(name, param.data)`. When using vLLM, ensure the GPUs assigned for training and generation are separate to avoid NCCL communication conflicts. If you do not set the `CUDA_VISIBLE_DEVICES` environment variable, the training script will use all available GPUs by default, which may lead to device conflicts. Starting from TRL next release after v0.19.1, the code automatically detects and prevents same-device usage, raising a error at the vllm server process: ```log RuntimeError: Attempting to use the same CUDA device for multiple distinct roles/ranks within the same communicator. Ensure that trainer is using different devices than vLLM server. ``` For example, if you want to use GPUs 4–7 for training while the server runs on GPUs 0-3, set: ```sh CUDA_VISIBLE_DEVICES=4,5,6,7 accelerate launch train.py ``` ## Advanced usage ### 🍷 More customization options with vLLM? You can customize the server configuration by passing additional arguments. ```txt $ trl vllm-serve --help usage: trl vllm-serve [-h] --model MODEL [--revision REVISION] [--tensor_parallel_size TENSOR_PARALLEL_SIZE] [--data_parallel_size DATA_PARALLEL_SIZE] [--host HOST] [--port PORT] [--gpu_memory_utilization GPU_MEMORY_UTILIZATION] [--dtype DTYPE] [--max_model_len MAX_MODEL_LEN] [--enable_prefix_caching ENABLE_PREFIX_CACHING] [--enforce_eager [ENFORCE_EAGER]] [--kv_cache_dtype KV_CACHE_DTYPE] [--trust_remote_code [TRUST_REMOTE_CODE]] [--log_level LOG_LEVEL] [--vllm_model_impl VLLM_MODEL_IMPL] options: -h, --help show this help message and exit --model MODEL Model name or path to load the model from. (default: None) --revision REVISION Revision to use for the model. If not specified, the default branch will be used. (default: None) --tensor_parallel_size TENSOR_PARALLEL_SIZE, --tensor-parallel-size TENSOR_PARALLEL_SIZE Number of tensor parallel workers to use. (default: 1) --data_parallel_size DATA_PARALLEL_SIZE, --data-parallel-size DATA_PARALLEL_SIZE Number of data parallel workers to use. For dense models, keep this at 1. Starting from vLLM `0.14.0`, setting this above `1` for dense models is no longer supported/useful and will error out (see vLLM PR #30739). (default: 1) --host HOST Host address to run the server on. (default: 0.0.0.0) --port PORT Port to run the server on. (default: 8000) --gpu_memory_utilization GPU_MEMORY_UTILIZATION, --gpu-memory-utilization GPU_MEMORY_UTILIZATION Ratio (between 0 and 1) of GPU memory to reserve for the model weights, activations, and KV cache on the device dedicated to generation powered by vLLM. Higher values will increase the KV cache size and thus improve the model's throughput. However, if the value is too high, it may cause out-of-memory (OOM) errors during initialization. (default: 0.9) --dtype DTYPE Data type to use for vLLM generation. If set to 'auto', the data type will be automatically determined based on the model configuration. Find the supported values in the vLLM documentation. (default: auto) --max_model_len MAX_MODEL_LEN, --max-model-len MAX_MODEL_LEN If set, the `max_model_len` to use for vLLM. This can be useful when running with reduced `vllm_gpu_memory_utilization`, leading to a reduced KV cache size. If not set, vLLM will use the model context size, which might be much larger than the KV cache, leading to inefficiencies. (default: None) --enable_prefix_caching ENABLE_PREFIX_CACHING, --enable-prefix-caching ENABLE_PREFIX_CACHING Whether to enable prefix caching in vLLM. If set to `True`, ensure that the model and the hardware support this feature. (default: None) --enforce_eager [ENFORCE_EAGER], --enforce-eager [ENFORCE_EAGER] Whether to enforce eager execution. If set to `True`, we will disable CUDA graph and always execute the model in eager mode. If `False` (default behavior), we will use CUDA graph and eager execution in hybrid. (default: False) --kv_cache_dtype KV_CACHE_DTYPE, --kv-cache-dtype KV_CACHE_DTYPE Data type to use for KV cache. If set to 'auto', the dtype will default to the model data type. (default: auto) --trust_remote_code [TRUST_REMOTE_CODE], --trust-remote-code [TRUST_REMOTE_CODE] Whether to trust remote code when loading models. Set to True to allow executing code from model repositories. This is required for some custom models but introduces security risks. (default: False) --log_level LOG_LEVEL, --log-level LOG_LEVEL Log level for uvicorn. Possible choices: 'critical', 'error', 'warning', 'info', 'debug', 'trace'. (default: info) --vllm_model_impl VLLM_MODEL_IMPL, --vllm-model-impl VLLM_MODEL_IMPL Model implementation to use for vLLM. Must be one of `transformers` or `vllm`. `transformers`: Use the `transformers` backend for model implementation. `vllm`: Use the `vllm` library for model implementation. (default: vllm) ``` ### 💆🏻‍♀️ What's the best distributed setup? ![tp dp throughput 8 gpus](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/tp_dp_throughput_8_gpus.png) ![tp dp throughput 4 gpus](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/tp_dp_throughput_4_gpus.png) > [!WARNING] > The benchmark plots above were collected with older vLLM versions. Starting with [vLLM PR #30739](https://github.com/vllm-project/vllm/pull/30739) (released in `0.14.0`), offline data parallel scaling for non-MoE (dense) models is no longer supported. To follow the latest recommendations, do not scale DP for non-MoE models. ### vLLM with Transformers Backend vLLM can use the **Transformers backend** for model implementations, which works for both LLMs and VLMs. To enable this, set `vllm_model_impl="transformers"` in your configuration or pass it via the command-line argument. For more details, check out [vLLM Transformers Backend](https://blog.vllm.ai/2025/04/11/transformers-backend.html). Example: ```sh CUDA_DEVICE_ORDER=PCI_BUS_ID CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen 2.5-VL-3B-Instruct --tensor-parallel-size 1 --port 8000 --enforce_eager --vllm_model_impl transformers ``` ### Modes of Using vLLM During Training TRL supports **two modes** for integrating vLLM during training: **colocate mode** (default) and **server mode**. #### Colocate Mode In **colocate mode**, vLLM runs inside the trainer process and shares GPU memory with the training model. This avoids launching a separate server and can improve GPU utilization, but may lead to memory contention on the training GPUs. This is the default mode. Example configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` ```python from trl.experimental.online_dpo import OnlineDPOConfig training_args = OnlineDPOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` ```python from trl.experimental.nash_md import NashMDConfig training_args = NashMDConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` ```python from trl.experimental.xpo import XPOConfig training_args = XPOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` ```python from trl import RLOOConfig training_args = RLOOConfig( ..., use_vllm=True, # vllm_mode="colocate" by default ) ``` #### Server Mode In **server mode**, vLLM runs as a separate process on dedicated GPUs and communicates with the trainer via HTTP. This setup is ideal if you have GPUs dedicated to inference. Example configuration: ```python from trl import GRPOConfig training_args = GRPOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` ```python from trl.experimental.online_dpo import OnlineDPOConfig training_args = OnlineDPOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` ```python from trl.experimental.nash_md import NashMDConfig training_args = NashMDConfig( ..., use_vllm=True, vllm_mode="server", ) ``` ```python from trl.experimental.xpo import XPOConfig training_args = XPOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` ```python from trl import RLOOConfig training_args = RLOOConfig( ..., use_vllm=True, vllm_mode="server", ) ``` > [!WARNING] > Check the documentation of the trainer you are using for specific details on vLLM usage and parameters. > [!WARNING] > To reduce GPU memory usage when running vLLM, consider [enabling vLLM sleep mode](reducing_memory_usage#vllm-sleep-mode). ================================================ FILE: docs/source/winrate_callback.md ================================================ # WinRateCallback [[autodoc]] experimental.winrate_callback.WinRateCallback ================================================ FILE: docs/source/xpo_trainer.md ================================================ # XPO Trainer [![model badge](https://img.shields.io/badge/All_models-XPO-blue)](https://huggingface.co/models?other=xpo,trl) ## Overview Exploratory Preference Optimization (XPO) was proposed in the paper [Exploratory Preference Optimization: Harnessing Implicit Q*-Approximation for Sample-Efficient RLHF](https://huggingface.co/papers/2405.21046) by Tengyang Xie, Dylan J. Foster, Akshay Krishnamurthy, [Corby Rosset](https://huggingface.co/corbyrosset), [Ahmed Awadallah](https://huggingface.co/AhmedAwadallah), and Alexander Rakhlin. It is a simple online preference tuning method based on the DPO loss together with a reward model (RM). XPO augments the DPO objective with an exploration bonus allowing the method to explore outside the support of the initial model and human feedback data. The abstract from the paper is the following: > Reinforcement learning from human feedback (RLHF) has emerged as a central tool for language model alignment. We consider online exploration in RLHF, which exploits interactive access to human or AI feedback by deliberately encouraging the model to produce diverse, maximally informative responses. By allowing RLHF to confidently stray from the pre-trained model, online exploration offers the possibility of novel, potentially super-human capabilities, but its full potential as a paradigm for language model training has yet to be realized, owing to computational and statistical bottlenecks in directly adapting existing reinforcement learning techniques. We propose a new algorithm for online exploration in RLHF, Exploratory Preference Optimization (XPO), which is simple and practical -- a one-line change to (online) Direct Preference Optimization (DPO; Rafailov et al., 2023) -- yet enjoys the strongest known provable guarantees and promising empirical performance. XPO augments the DPO objective with a novel and principled exploration bonus, empowering the algorithm to explore outside the support of the initial model and human feedback data. In theory, we show that XPO is provably sample-efficient and converges to a near-optimal language model policy under natural exploration conditions, irrespective of whether the initial model has good coverage. Our analysis, which builds on the observation that DPO implicitly performs a form of Q*-approximation (or, Bellman error minimization), combines previously disparate techniques from language modeling and theoretical reinforcement learning in a serendipitous fashion through the perspective of KL-regularized Markov decision processes. Empirically, we find that XPO is more sample-efficient than non-exploratory DPO variants in a preliminary evaluation. This post-training method was contributed by [Kashif Rasul](https://huggingface.co/kashif), [Quentin Gallouédec](https://huggingface.co/qgallouedec) and [Lewis Tunstall](https://huggingface.co/lewtun). > [!NOTE] > XPO is currently experimental. The API may change without notice while the feature is iterated on. ## Quick start This example demonstrates how to train a model using the XPO method. We use the [Qwen 0.5B model](https://huggingface.co/Qwen/Qwen2-0.5B-Instruct) as the base model and [`experimental.judges.PairRMJudge`] as a judge. We use the prompts from the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback). You can view the prompts in the dataset here: Below is the script to train the model: ```python # train_xpo.py from datasets import load_dataset from trl.experimental.judges import PairRMJudge from trl.experimental.xpo import XPOConfig, XPOTrainer from transformers import AutoModelForCausalLM, AutoTokenizer model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-0.5B-Instruct") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct") judge = PairRMJudge() train_dataset = load_dataset("trl-lib/ultrafeedback-prompt", split="train") training_args = XPOConfig(output_dir="Qwen2-0.5B-XPO") trainer = XPOTrainer( model=model, judge=judge, args=training_args, processing_class=tokenizer, train_dataset=train_dataset ) trainer.train() ``` Execute the script using the following command: ```bash accelerate launch train_xpo.py ``` Distributed across 8 GPUs, the training takes approximately 1 hour. To see how the [trained model](https://huggingface.co/trl-lib/Qwen2-0.5B-XPO) performs, you can use the [Transformers Chat CLI](https://huggingface.co/docs/transformers/quicktour#chat-with-text-generation-models).
$ transformers chat trl-lib/Qwen2-0.5B-XPO
<quentin_gallouedec>:
What is the best programming language?

<trl-lib/Qwen2-0.5B-XPO>:
The best programming language depends on individual preferences and familiarity with coding concepts. Some popular languages include Python, Java, C++, and JavaScript.
## Expected dataset type XPO requires a [prompt-only dataset](dataset_formats#prompt-only). The [`experimental.xpo.XPOTrainer`] supports both [conversational](dataset_formats#conversational) and [standard](dataset_formats#standard) dataset format. When provided with a conversational dataset, the trainer will automatically apply the chat template to the dataset. ## Usage tips ### Use a reward model Instead of a judge, you can chose to use a reward model -- see [Reward Bench](https://huggingface.co/spaces/allenai/reward-bench) for a leaderboard of public models you can use. Below is a code example showing how to replace a judge with the [trl-lib/Qwen2-0.5B-Reward](https://huggingface.co/trl-lib/Qwen2-0.5B-Reward) model: ```diff - from trl.experimental.judges import PairRMJudge + from transformers import AutoModelForSequenceClassification - judge = PairRMJudge() + reward_model = AutoModelForSequenceClassification.from_pretrained("trl-lib/Qwen2-0.5B-Reward", num_labels=1) trainer = XPOTrainer( ... - judge=judge, + reward_funcs=reward_model, ) ``` > [!WARNING] > Make sure that the SFT model and reward model use the _same_ chat template and the same tokenizer. Otherwise, you may find the model completions are scored incorrectly during training. ### Encourage EOS token generation When using a reward model, we may want the model to generate completions within a given length. During training, the model will generate completions up to the maximum length specified in the `max_new_tokens` argument of [`experimental.xpo.XPOConfig`]. If you want to penalize the model for not generating an EOS token before reaching the maximum length, you can use the `missing_eos_penalty` argument of [`experimental.xpo.XPOConfig`]: ```python training_args = XPOConfig(..., max_new_tokens=128, missing_eos_penalty=1.0) ``` ### Logging Completions To better understand your model’s behavior during training, you can log sample completions periodically using the [`LogCompletionsCallback`]. ```python trainer = XPOTrainer(..., eval_dataset=eval_dataset) completions_callback = LogCompletionsCallback(trainer, num_prompts=8) trainer.add_callback(completions_callback) ``` This callback logs the model's generated completions directly to Weights & Biases. ![Logged Completions](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/wandb_completions.png) ## Example script We provide an example script to train a model using the XPO method. The script is available in [`examples/scripts/xpo.py`](https://github.com/huggingface/trl/blob/main/examples/scripts/xpo.py) To test the XPO script with the [Qwen2.5 0.5B model](https://huggingface.co/trl-lib/Qwen/Qwen2.5-0.5B-Instruct) on the [UltraFeedback dataset](https://huggingface.co/datasets/openbmb/UltraFeedback), run the following command: ```bash python examples/scripts/xpo.py \ --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --judge pair_rm \ --dataset_name trl-lib/ultrafeedback-prompt \ --learning_rate 5.0e-7 \ --output_dir Qwen2.5-0.5B-XPO-PairRM \ --warmup_steps 0.1 \ --push_to_hub ``` ## Logged metrics While training and evaluating we record the following reward metrics: * `loss/xpo`: The mean xpo part of the full loss. * `loss/dpo`: The mean dpo part of the full loss. * `objective/kl`: The mean KL divergence between the model and reference data. * `objective/entropy`: The mean entropy of the model and reference data. * `objective/model_scores`: The mean scores (according to the reward model) of the model completions. * `objective/ref_scores`: The mean scores (according to the reward model) of the reference completions. * `objective/scores_margin`: The mean score margin (according to the external reward model) between the chosen and rejected completions. * `rewards/chosen`: The mean reward (according to XPO's DPO implicit reward model) of the chosen completions. * `rewards/rejected`: The mean reward (according to XPO's DPO implicit reward model) of the rejected completions. * `rewards/accuracies`: The accuracies of the XPO's implicit reward model. * `rewards/margins`: The mean reward margin (according to online DPO's implicit reward model) between the chosen and rejected completions. * `logps/chosen`: The mean log probabilities of the chosen completions. * `logps/rejected`: The mean log probabilities of the rejected completions. * `val/model_contain_eos_token`: The amount of times the model's output contains the eos token. * `val/ref_contain_eos_token`: The amount of times the reference's output contains the eos token. * `alpha`: The weight of the XPO loss term. Typically fixed, but can be made dynamic by passing a list to [`experimental.xpo.XPOConfig`]. * `beta`: The parameter that controls the weight of the loss term representing the deviation from the reference model. Typically fixed, but can be made dynamic by passing a list to [`experimental.xpo.XPOConfig`]. ## XPOTrainer [[autodoc]] experimental.xpo.XPOTrainer - train - save_model - push_to_hub ## XPOConfig [[autodoc]] experimental.xpo.XPOConfig ================================================ FILE: examples/README.md ================================================ # Examples Please check out https://huggingface.co/docs/trl/example_overview for documentation on our examples. ================================================ FILE: examples/accelerate_configs/alst_ulysses_4gpu.yaml ================================================ # ALST/Ulysses Sequence Parallelism with 2D Parallelism (DP + SP) for 4 GPUs # # This configuration enables 2D parallelism: # - Sequence Parallelism (sp_size=2): Sequences split across 2 GPUs using ALST/Ulysses # - Data Parallelism (dp_shard_size=2): Model/optimizer sharded across 2 GPUs # - Total: 4 GPUs (2 × 2) # # Set parallelism_config in your training script: # parallelism_config = ParallelismConfig( # sp_backend="deepspeed", # sp_size=2, # dp_shard_size=2, # Calculated as: num_gpus // sp_size # sp_handler=DeepSpeedSequenceParallelConfig(...) # ) compute_environment: LOCAL_MACHINE debug: false deepspeed_config: zero_stage: 3 seq_parallel_communication_data_type: bf16 offload_optimizer_device: none offload_param_device: none zero3_init_flag: true zero3_save_16bit_model: true distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 4 # Total number of GPUs rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false parallelism_config: parallelism_config_dp_replicate_size: 1 parallelism_config_dp_shard_size: 2 # Enables 2D parallelism with SP parallelism_config_tp_size: 1 parallelism_config_sp_size: 2 # Sequence parallel size parallelism_config_sp_backend: deepspeed parallelism_config_sp_seq_length_is_variable: true parallelism_config_sp_attn_implementation: flash_attention_2 ================================================ FILE: examples/accelerate_configs/context_parallel_2gpu.yaml ================================================ # Context Parallelism with FSDP for 2 GPUs compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: true # Enable activation checkpointing for memory efficiency fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_cpu_ram_efficient_loading: true fsdp_offload_params: false fsdp_reshard_after_forward: true fsdp_state_dict_type: FULL_STATE_DICT fsdp_version: 2 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 2 # Number of GPUs rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false parallelism_config: parallelism_config_dp_replicate_size: 1 parallelism_config_dp_shard_size: 1 parallelism_config_tp_size: 1 parallelism_config_cp_size: 2 # Context parallel size ================================================ FILE: examples/accelerate_configs/deepspeed_zero1.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard gradient_accumulation_steps: 1 zero3_init_flag: false zero_stage: 1 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/deepspeed_zero2.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard offload_optimizer_device: none offload_param_device: none zero3_init_flag: false zero_stage: 2 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/deepspeed_zero3.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard offload_optimizer_device: none offload_param_device: none zero3_init_flag: true zero3_save_16bit_model: true zero_stage: 3 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/fsdp1.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: false fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_backward_prefetch: BACKWARD_PRE fsdp_cpu_ram_efficient_loading: true fsdp_forward_prefetch: true fsdp_offload_params: false fsdp_reshard_after_forward: FULL_SHARD fsdp_state_dict_type: FULL_STATE_DICT fsdp_sync_module_states: true fsdp_use_orig_params: true fsdp_version: 1 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/fsdp2.yaml ================================================ # Requires accelerate 1.7.0 or higher compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: false fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_cpu_ram_efficient_loading: true fsdp_offload_params: false fsdp_reshard_after_forward: true fsdp_state_dict_type: FULL_STATE_DICT fsdp_version: 2 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/multi_gpu.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: MULTI_GPU downcast_bf16: 'no' gpu_ids: all machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/accelerate_configs/single_gpu.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: "NO" downcast_bf16: 'no' gpu_ids: all machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 1 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/cli_configs/example_config.yaml ================================================ # This is an example configuration file of TRL CLI, you can use it for # SFT like that: `trl sft --config config.yaml --output_dir test-sft` # The YAML file supports environment variables by adding an `env` field # as below # env: # CUDA_VISIBLE_DEVICES: 0 model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: stanfordnlp/imdb report_to: none learning_rate: 0.0001 lr_scheduler_type: cosine ================================================ FILE: examples/datasets/deepmath_103k.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/DeepMath-103K"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/DeepMath-103K", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def process_example(example): solution = example["final_answer"] if solution not in ["True", "False", "Yes", "No"]: solution = f"${solution}$" prompt = [{"role": "user", "content": example["question"]}] return {"prompt": prompt, "solution": solution} model_card = ModelCard(""" --- tags: [trl] --- # DeepMath-103K Dataset ## Summary [DeepMath-103K](https://huggingface.co/datasets/zwhe99/DeepMath-103K) is meticulously curated to push the boundaries of mathematical reasoning in language models. ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Prompt-only](https://huggingface.co/docs/trl/main/dataset_formats#prompt-only) Column: - `"prompt"`: The input question. - `"solution"`: The solution to the math problem. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/deepmath_103k.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("zwhe99/DeepMath-103K", split="train") dataset = dataset.map( process_example, remove_columns=dataset.column_names, num_proc=script_args.dataset_num_proc, ) dataset = dataset.train_test_split(test_size=0.05, seed=42) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/hh-rlhf-helpful-base.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/hh-rlhf-helpful-base"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/hh-rlhf-helpful-base", metadata={"help": "Hugging Face repository ID to push the dataset to."} ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."} ) def common_start(str1: str, str2: str) -> str: # Zip the two strings and iterate over them together common_chars = [] for c1, c2 in zip(str1, str2, strict=True): if c1 == c2: common_chars.append(c1) else: break # Join the common characters and return as a string return "".join(common_chars) def extract_dialogue(example: str) -> list[dict[str, str]]: # Extract the prompt, which corresponds to the common start of the chosen and rejected dialogues prompt_text = common_start(example["chosen"], example["rejected"]) # The chosen and rejected may share a common start, so we need to remove the common part if not prompt_text.endswith("\n\nAssistant: "): prompt_text = prompt_text[: prompt_text.rfind("\n\nAssistant: ")] + "\n\nAssistant: " # Extract the chosen and rejected lines chosen_line = example["chosen"][len(prompt_text) :] rejected_line = example["rejected"][len(prompt_text) :] # Remove the generation prompt ("\n\nAssistant: ") from the prompt prompt_text = prompt_text[: -len("\n\nAssistant: ")] # Split the string at every occurrence of "Human: " or "Assistant: " prompt_lines = re.split(r"(\n\nAssistant: |\n\nHuman: )", prompt_text) # Remove the first element as it's empty prompt_lines = prompt_lines[1:] prompt = [] for idx in range(0, len(prompt_lines), 2): role = "user" if prompt_lines[idx] == "\n\nHuman: " else "assistant" content = prompt_lines[idx + 1] prompt.append({"role": role, "content": content}) # Remove the prompt from the chosen and rejected dialogues chosen = [{"role": "assistant", "content": chosen_line}] rejected = [{"role": "assistant", "content": rejected_line}] return {"prompt": prompt, "chosen": chosen, "rejected": rejected} model_card = ModelCard(""" --- tags: [trl] --- # HH-RLHF-Helpful-Base Dataset ## Summary The HH-RLHF-Helpful-Base dataset is a processed version of [Anthropic's HH-RLHF](https://huggingface.co/datasets/Anthropic/hh-rlhf) dataset, specifically curated to train models using the [TRL library](https://github.com/huggingface/trl) for preference learning and alignment tasks. It contains pairs of text samples, each labeled as either "chosen" or "rejected," based on human preferences regarding the helpfulness of the responses. This dataset enables models to learn human preferences in generating helpful responses, enhancing their ability to assist users effectively. ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Preference](https://huggingface.co/docs/trl/main/dataset_formats#preference) Columns: - `"prompt"`: The user query. - `"chosen"`: A response deemed helpful by human evaluators. - `"rejected"`: A response considered less helpful or unhelpful. This structure allows models to learn to prefer the _chosen_ response over the _rejected_ one, thereby aligning with human preferences in helpfulness. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/hh-rlhf-helpful-base.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("Anthropic/hh-rlhf", data_dir="helpful-base") dataset = dataset.map(extract_dialogue, num_proc=script_args.dataset_num_proc) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/llava_instruct_mix.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import ast from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/llava-instruct-mix"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/llava-instruct-mix", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def process_example(example): messages = [] for message in ast.literal_eval(example["conversations"]): content = message["value"] content = content.replace("", "").strip() role = "user" if message["from"] == "human" else "assistant" messages.append({"role": role, "content": content}) return {"messages": messages, "images": [example["image"]]} def filter_long_examples(example): total_length = sum(len(msg["content"]) for msg in example["messages"]) return total_length <= 1000 def split_prompt_completion(example): """ Splits the messages into a prompt and a completion. The last message is considered the completion. """ assert len(example["messages"]) > 1 example["prompt"] = example["messages"][:-1] example["completion"] = example["messages"][-1:] return example model_card = ModelCard(""" --- tags: [trl] --- # LLaVA Instruct Mix ## Summary The LLaVA Instruct Mix dataset is a processed version of [LLaVA Instruct Mix](https://huggingface.co/datasets/theblackcat102/llava-instruct-mix). ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Language-modeling](https://huggingface.co/docs/trl/main/dataset_formats#language-modeling) Columns: - `"images"`: The image associated with the text. - `"prompt"`: A list of messages that form the context for the conversation. - `"completion"`: The last message in the conversation, which is the model's response. This structure allows models to learn from the context of the conversation, enhancing their understanding of how to generate descriptive text based on visual inputs. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/llava_instruct_mix.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("theblackcat102/llava-instruct-mix", split="train", num_proc=script_args.dataset_num_proc) dataset = dataset.map( process_example, remove_columns=["conversations", "image"], num_proc=script_args.dataset_num_proc ) dataset = dataset.filter(filter_long_examples, num_proc=script_args.dataset_num_proc) dataset = dataset.map(split_prompt_completion, remove_columns=["messages"], num_proc=script_args.dataset_num_proc) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id, num_proc=script_args.dataset_num_proc) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/lm-human-preferences-descriptiveness.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import AutoTokenizer, HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/lm-human-preferences-descriptiveness"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/lm-human-preferences-descriptiveness", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) # Edge cases handling: remove the cases where all samples are the same def samples_not_all_same(example): return not all(example["sample0"] == example[f"sample{j}"] for j in range(1, 4)) def to_prompt_completion(example, tokenizer): prompt = tokenizer.decode(example["query"]).strip() best_idx = example["best"] chosen = tokenizer.decode(example[f"sample{best_idx}"]) for rejected_idx in range(4): # take the first rejected sample that is different from the chosen one rejected = tokenizer.decode(example[f"sample{rejected_idx}"]) if chosen != rejected: break assert chosen != rejected return {"prompt": prompt, "chosen": chosen, "rejected": rejected} model_card = ModelCard(""" --- tags: [trl] --- # LM-Human-Preferences-Descriptiveness Dataset ## Summary The LM-Human-Preferences-Descriptiveness dataset is a processed subset of [OpenAI's LM-Human-Preferences](https://github.com/openai/lm-human-preferences), focusing specifically on enhancing the descriptiveness of generated text. It contains pairs of text samples, each labeled as either "chosen" or "rejected," based on human preferences regarding the level of detail and vividness in the descriptions. This dataset enables models to learn human preferences in descriptive language, improving their ability to generate rich and engaging narratives. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Preference](https://huggingface.co/docs/trl/main/dataset_formats#preference) Columns: - `"prompt"`: The text sample. - `"chosen"`: A version of the text with enhanced descriptiveness. - `"rejected"`: A version of the text with less descriptiveness. This structure allows models to learn to prefer the _chosen_ response over the _rejected_ one, thereby aligning with human preferences in descriptive language. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/lm-human-preferences-descriptiveness.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset( "json", data_files="https://openaipublic.blob.core.windows.net/lm-human-preferences/labels/descriptiveness/offline_5k.json", split="train", ) dataset = dataset.filter(samples_not_all_same, num_proc=script_args.dataset_num_proc) dataset = dataset.map( to_prompt_completion, num_proc=script_args.dataset_num_proc, remove_columns=["query", "sample0", "sample1", "sample2", "sample3", "best"], fn_kwargs={"tokenizer": AutoTokenizer.from_pretrained("gpt2")}, ) # train_size taken from https://github.com/openai/lm-human-preferences/blob/cbfd210bb8b08f6bc5c26878c10984b90f516c66/launch.py#L79) dataset = dataset.train_test_split(train_size=4992) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/lm-human-preferences-sentiment.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import AutoTokenizer, HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/lm-human-preferences-sentiment"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/lm-human-preferences-sentiment", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_prompt_completion(example, tokenizer): prompt = tokenizer.decode(example["query"]).strip() best_idx = example["best"] chosen = tokenizer.decode(example[f"sample{best_idx}"]) for rejected_idx in range(4): # take the first rejected sample that is different from the chosen one rejected = tokenizer.decode(example[f"sample{rejected_idx}"]) if chosen != rejected: break assert chosen != rejected return {"prompt": prompt, "chosen": chosen, "rejected": rejected} model_card = ModelCard(""" --- tags: [trl] --- # LM-Human-Preferences-Sentiment Dataset ## Summary The LM-Human-Preferences-Sentiment dataset is a processed subset of [OpenAI's LM-Human-Preferences](https://github.com/openai/lm-human-preferences), focusing specifically on sentiment analysis tasks. It contains pairs of text samples, each labeled as either "chosen" or "rejected," based on human preferences regarding the sentiment conveyed in the text. This dataset enables models to learn human preferences in sentiment expression, enhancing their ability to generate and evaluate text with desired emotional tones. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Preference](https://huggingface.co/docs/trl/main/dataset_formats#preference) Columns: - `"prompt"`: The text sample. - `"chosen"`: A version of the text that conveys the desired sentiment. - `"rejected"`: A version of the text that does not convey the desired sentiment. This structure allows models to learn to prefer the _chosen_ response over the _rejected_ one, thereby aligning with human preferences in sentiment expression. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/lm-human-preferences-sentiment.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset( "json", data_files="https://openaipublic.blob.core.windows.net/lm-human-preferences/labels/sentiment/offline_5k.json", split="train", ) dataset = dataset.map( to_prompt_completion, num_proc=script_args.dataset_num_proc, remove_columns=["query", "sample0", "sample1", "sample2", "sample3", "best"], fn_kwargs={"tokenizer": AutoTokenizer.from_pretrained("gpt2")}, ) # train_size taken from https://github.com/openai/lm-human-preferences/blob/cbfd210bb8b08f6bc5c26878c10984b90f516c66/launch.py#L70) dataset = dataset.train_test_split(train_size=4992) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/math_shepherd.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from dataclasses import dataclass, field from itertools import chain from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/math_shepherd"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/math_shepherd", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def process_example(example): # Replace "ки" with "ⶻ" so that the size of the "input" matches the size of the "label" inputs = example["input"].replace("ки", "ⶻ") # Find the indices of the "ⶻ" characters (that should match with the indexes of the "+" or "-" in the label) indexes = [m.start() for m in re.finditer("ⶻ", inputs)] # Sanity that all indexes are either "+" or "-" assert all(example["label"][idx] in ["+", "-"] for idx in indexes) # Get the labels labels = [example["label"][idx] == "+" for idx in indexes] # Split the inputs into steps (caution, the first step is missing here, it is the prompt) steps = [inputs[i:j] for i, j in zip(chain([0], indexes), chain(indexes, [None]), strict=True)] # Remove the last step (single ⶻ) steps = steps[:-1] # Get the prompt (first part) and completions (rest) prompt = steps[0] completions = steps[1:] # Remove the heading "ⶻ" and the final whitespace from the completions assert all(completion.startswith("ⶻ") for completion in completions) completions = [completion[1:].strip() for completion in completions] # At this point, we need to retrieve the first step from the prompt. # First, we handle particular cases (annotation error) where we have a first label before the end of the prompt. if prompt.startswith( ( "Mr. Rocky", "Parker", "What is the smallest positive", " The Myth", "Let $\\mathbf{a}$", "Find the arithmetic", "Determine an ordered pair", "Determine the ordered pair", "At the Quill and Scroll stationery", "Round to the nearest", r"Calculate $\sqrt{10p}", r"Simplify $\sqrt{28x}", ) ): # Some spotted datasets errors where there is an annotation in the prompt: we remove it labels = labels[1:] # Then we handle the general case: we get the first step from the prompt by looking for "Step 1:" or "step 1:" or # (less common) "?". elif "Step 1:" in prompt: prompt, first_step = prompt.split("Step 1:") first_step = "Step 1:" + first_step completions = [first_step.strip()] + completions elif "step 1:" in prompt: prompt, first_step = prompt.split("step 1:") first_step = "step 1:" + first_step completions = [first_step.strip()] + completions elif "?" in prompt: prompt, first_step = prompt.split("?") prompt = prompt + "?" completions = [first_step.strip()] + completions else: raise ValueError(f"Prompt can't be processed: {prompt}") # Strip the prompt prompt = prompt.strip() # Sanity check that the length of the completions is the same as the length of the labels assert len(completions) == len(labels) return {"prompt": prompt, "completions": completions, "labels": labels} model_card = ModelCard(""" --- tags: [trl] --- # Math-Shepherd Dataset ## Summary The Math-Shepherd dataset is a processed version of [Math-Shepherd dataset](peiyi9979/Math-Shepherd), designed to train models using the [TRL library](https://github.com/huggingface/trl) for stepwise supervision tasks. It provides step-by-step solutions to mathematical problems, enabling models to learn and verify each step of a solution, thereby enhancing their reasoning capabilities. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Stepwise supervision](https://huggingface.co/docs/trl/main/dataset_formats#stepwise-supervision) Columns: - `"prompt"`: The problem statement. - `"completions"`: A list of reasoning steps generated to solve the problem. - `"labels"`: A list of booleans or floats indicating the correctness of each corresponding reasoning step. This structure allows models to learn the correctness of each step in a solution, facilitating improved reasoning and problem-solving abilities. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/math_shepherd.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("peiyi9979/Math-Shepherd", split="train") dataset = dataset.map( process_example, remove_columns=["input", "label", "task"], num_proc=script_args.dataset_num_proc, ) dataset = dataset.train_test_split(test_size=0.05, seed=42) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/prm800k.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/prm800k"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/prm800k", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def process_example(example): outputs = [] prompt = example["question"]["problem"] # Iterate through each step previous_completions = [] previous_labels = [] for step in example["label"]["steps"]: if step["completions"] is None and step["human_completion"] is None and step["chosen_completion"] is None: # happens sometimes break # Loop through completions for completion_idx, completion in enumerate(step["completions"]): # For every completion that are not chosen, we are in a terminal state, so we can add it to the list of outputs. if completion_idx != step["chosen_completion"]: content = completion["text"] completions = previous_completions[:] + [content] label = completion["rating"] == 1 labels = previous_labels[:] + [label] outputs.append({"prompt": prompt, "completions": completions, "labels": labels}) # Now, expand the previous completions and labels if step["chosen_completion"] is not None: chosen_completion = step["completions"][step["chosen_completion"]] label = chosen_completion["rating"] == 1 elif step["human_completion"] is not None: chosen_completion = step["human_completion"] label = True else: break content = chosen_completion["text"] previous_completions.append(content) previous_labels.append(label) # Last step: we are in a terminal state, so we can add it to the list of outputs outputs.append({"prompt": prompt, "completions": previous_completions, "labels": previous_labels}) return outputs def process_batch(examples): outputs = [] batch_size = len(examples["label"]) for idx in range(batch_size): example = {k: v[idx] for k, v in examples.items()} outputs.extend(process_example(example)) # list of dict to dict of list outputs = {k: [v[k] for v in outputs] for k in outputs[0]} return outputs model_card = ModelCard(""" --- tags: [trl] --- # PRM800K Dataset ## Summary The PRM800K dataset is a processed version of [OpenAI's PRM800K](https://github.com/openai/prm800k), designed to train models using the [TRL library](https://github.com/huggingface/trl) for stepwise supervision tasks. It contains 800,000 step-level correctness labels for model-generated solutions to problems from the MATH dataset. This dataset enables models to learn and verify each step of a solution, enhancing their reasoning capabilities. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Stepwise supervision](https://huggingface.co/docs/trl/main/dataset_formats#stepwise-supervision) Columns: - `"prompt"`: The problem statement. - `"completions"`: A list of reasoning steps generated to solve the problem. - `"labels"`: A list of booleans or floats indicating the correctness of each corresponding reasoning step. This structure allows models to learn the correctness of each step in a solution, facilitating improved reasoning and problem-solving abilities. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/prm800k.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] data_files = { "train": "https://github.com/openai/prm800k/raw/refs/heads/main/prm800k/data/phase1_train.jsonl", "test": "https://github.com/openai/prm800k/raw/refs/heads/main/prm800k/data/phase1_test.jsonl", } dataset = load_dataset("json", data_files=data_files) dataset = dataset.map( process_batch, batched=True, batch_size=10, remove_columns=[ "labeler", "timestamp", "generation", "is_quality_control_question", "is_initial_screening_question", "question", "label", ], num_proc=script_args.dataset_num_proc, ) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/rlaif-v.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import features, load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/rlaif-v"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/rlaif-v", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_conversational(example): """ Convert prompt from "xxx" to [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "xxx"}]}] and chosen and rejected from "xxx" to [{"role": "assistant", "content": [{"type": "text", "text": "xxx"}]}]. Images are wrapped into a list. """ prompt = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": example["question"]}]}] chosen = [{"role": "assistant", "content": [{"type": "text", "text": example["chosen"]}]}] rejected = [{"role": "assistant", "content": [{"type": "text", "text": example["rejected"]}]}] return {"prompt": prompt, "images": [example["image"]], "chosen": chosen, "rejected": rejected} model_card = ModelCard(""" --- tags: [trl] --- # RLAIF-V Dataset ## Summary The RLAIF-V dataset is a processed version of the [openbmb/RLAIF-V-Dataset](https://huggingface.co/datasets/openbmb/RLAIF-V-Dataset#dataset-card-for-rlaif-v-dataset), specifically curated to train vision-language models using the [TRL library](https://github.com/huggingface/trl) for preference learning tasks. It contains 83,132 high-quality comparison pairs, each comprising an image and two textual descriptions: one preferred and one rejected. This dataset enables models to learn human preferences in visual contexts, enhancing their ability to generate and evaluate image captions. ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Preference](https://huggingface.co/docs/trl/main/dataset_formats#preference) Columns: - `"prompt"`: The task related to the image. - `"images"`: The image. - `"chosen"`: The preferred answer. - `"rejected"`: An alternative answer that was not preferred. This structure allows models to learn to prefer the _chosen_ response over the _rejected_ one, thereby aligning with human preferences in visual tasks. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/rlaif-v.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("openbmb/RLAIF-V-Dataset", split="train") dataset = dataset.map( to_conversational, num_proc=script_args.dataset_num_proc, remove_columns=dataset.column_names, writer_batch_size=128, ) # Cast the images to Sequence[Image] to avoid bytes format f = dataset.features f["images"] = features.Sequence(features.Image(decode=True)) dataset = dataset.cast(f) dataset = dataset.train_test_split(test_size=0.01, writer_batch_size=128) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/tldr.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/tldr"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/tldr", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_prompt_completion(example): tldr_format_str = "SUBREDDIT: r/{subreddit}\n\nTITLE: {title}\n\nPOST: {post}\n\nTL;DR:" prompt = tldr_format_str.format(subreddit=example["subreddit"], title=example["title"], post=example["post"]) completion = " " + example["summary"] # Add a space to separate the prompt from the completion return {"prompt": prompt, "completion": completion} model_card = ModelCard(""" --- tags: [trl] --- # TL;DR Dataset ## Summary The TL;DR dataset is a processed version of Reddit posts, specifically curated to train models using the [TRL library](https://github.com/huggingface/trl) for summarization tasks. It leverages the common practice on Reddit where users append "TL;DR" (Too Long; Didn't Read) summaries to lengthy posts, providing a rich source of paired text data for training summarization models. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Prompt-completion](https://huggingface.co/docs/trl/main/dataset_formats#prompt-completion) Columns: - `"prompt"`: The unabridged Reddit post. - `"completion"`: The concise "TL;DR" summary appended by the author. This structure enables models to learn the relationship between detailed content and its abbreviated form, enhancing their summarization capabilities. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/tldr.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] # Filtered reddit TL;DR dataset from https://github.com/openai/summarize-from-feedback?tab=readme-ov-file#reddit-tldr-dataset data_files = { "train": "https://openaipublic.blob.core.windows.net/summarize-from-feedback/datasets/tldr_3_filtered/train.jsonl", "validation": "https://openaipublic.blob.core.windows.net/summarize-from-feedback/datasets/tldr_3_filtered/valid.jsonl", "test": "https://openaipublic.blob.core.windows.net/summarize-from-feedback/datasets/tldr_3_filtered/test.jsonl", } dataset = load_dataset("json", data_files=data_files) dataset = dataset.map( to_prompt_completion, num_proc=script_args.dataset_num_proc, remove_columns=["id", "subreddit", "title", "post", "summary"], ) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/tldr_preference.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/tldr-preference"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/tldr-preference", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_preference(example): info = example["info"] if example["batch"] in ["batch0_cnndm", "cnndm0", "cnndm2"]: # CNN Daily Mail batches article = info["article"].replace("\n\n", "\n") prompt = f"TITLE: {info['title']}\n\n{article}\n\nTL;DR:" elif example["batch"] in [f"batch{i}" for i in range(3, 23)] + ["edit_b2_eval_test"]: # Reddit batches post = info["post"].replace("\n\n", "\n") prompt = f"SUBREDDIT: r/{info['subreddit']}\n\nTITLE: {info['title']}\n\nPOST: {post}\n\nTL;DR:" else: raise ValueError(f"Unknown batch: {example['batch']}") chosen_idx = example["choice"] rejected_idx = 1 - chosen_idx chosen = example["summaries"][chosen_idx]["text"] rejected = example["summaries"][rejected_idx]["text"] return {"prompt": prompt, "chosen": chosen, "rejected": rejected} model_card = ModelCard(""" --- tags: [trl] --- # TL;DR Dataset for Preference Learning ## Summary The TL;DR dataset is a processed version of Reddit posts, specifically curated to train models using the [TRL library](https://github.com/huggingface/trl) for preference learning and Reinforcement Learning from Human Feedback (RLHF) tasks. It leverages the common practice on Reddit where users append "TL;DR" (Too Long; Didn't Read) summaries to lengthy posts, providing a rich source of paired text data for training models to understand and generate concise summaries. ## Data Structure - **Format**: [Standard](https://huggingface.co/docs/trl/main/dataset_formats#standard) - **Type**: [Preference](https://huggingface.co/docs/trl/main/dataset_formats#preference) Columns: - `"prompt"`: The unabridged Reddit post. - `"chosen"`: The concise "TL;DR" summary appended by the author. - `"rejected"`: An alternative summary or response that was not selected. This structure enables models to learn the relationship between detailed content and its abbreviated form, enhancing their summarization capabilities. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/tldr_preference.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("openai/summarize_from_feedback", "comparisons") dataset = dataset.map( to_preference, num_proc=script_args.dataset_num_proc, remove_columns=["info", "summaries", "choice", "worker", "batch", "split", "extra"], ) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/ultrafeedback-prompt.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/ultrafeedback-prompt"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/ultrafeedback-prompt", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_unpaired_preference(example): prompt = [{"role": "user", "content": example["instruction"]}] return {"prompt": prompt} def drop_long_prompt(example): if len(example["prompt"][0]["content"]) > 512: return False else: return True model_card = ModelCard(""" --- tags: [trl] --- # UltraFeedback - Prompts Dataset ## Summary The UltraFeedback - Prompts dataset is a processed version of the [UltraFeedback](https://huggingface.co/datasets/openbmb/UltraFeedback) dataset for model evaluation on specific aspects like helpfulness, honesty, and instruction-following. ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Prompt-only](https://huggingface.co/docs/trl/main/dataset_formats#prompt-only) Column: - `"prompt"`: The input question or instruction provided to the model. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/ultrafeedback-prompt.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("openbmb/UltraFeedback", split="train") dataset = dataset.map( to_unpaired_preference, remove_columns=["source", "instruction", "models", "completions", "correct_answers", "incorrect_answers"], num_proc=script_args.dataset_num_proc, ) dataset = dataset.filter(drop_long_prompt) dataset = dataset.train_test_split(test_size=0.05, seed=42) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/datasets/ultrafeedback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import load_dataset from huggingface_hub import ModelCard from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: model_name (`str`, *optional*, defaults to `"gpt-3.5-turbo"`): Language model to target. Possible values are: aspect (`str`, *optional*, defaults to `"helpfulness"`): Aspect to target. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness"`): Hugging Face repository ID to push the dataset to. dataset_num_proc (`int`, *optional*): Number of workers to use for dataset processing. """ model_name: str = field( default="gpt-3.5-turbo", metadata={ "help": "Language model to target.", "choices": [ "alpaca-7b", "bard", "falcon-40b-instruct", "gpt-3.5-turbo", "gpt-4", "llama-2-13b-chat", "llama-2-70b-chat", "llama-2-7b-chat", "mpt-30b-chat", "pythia-12b", "starchat", "ultralm-13b", "ultralm-65b", "vicuna-33b", "wizardlm-13b", "wizardlm-70b", "wizardlm-7b", ], }, ) aspect: str = field( default="helpfulness", metadata={ "help": "Aspect to target. Possible values are: 'helpfulness' (default), 'honesty', " "'instruction-following', 'truthfulness'.", "choices": ["helpfulness", "honesty", "instruction-following", "truthfulness"], }, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of workers to use for dataset processing."}, ) def to_unpaired_preference(example, model_name, aspect): prompt = [{"role": "user", "content": example["instruction"]}] model_index = example["models"].index(model_name) response_content = example["completions"][model_index]["response"] completion = [{"role": "assistant", "content": response_content}] score = int(example["completions"][model_index]["annotations"][aspect]["Rating"]) label = score >= 5 return {"prompt": prompt, "completion": completion, "label": label} model_card = ModelCard(""" --- tags: [trl] --- # UltraFeedback GPT-3.5-Turbo Helpfulness Dataset ## Summary The UltraFeedback GPT-3.5-Turbo Helpfulness dataset contains processed user-assistant interactions filtered for helpfulness, derived from the [openbmb/UltraFeedback](https://huggingface.co/datasets/openbmb/UltraFeedback) dataset. It is designed for fine-tuning and evaluating models in alignment tasks. ## Data Structure - **Format**: [Conversational](https://huggingface.co/docs/trl/main/dataset_formats#conversational) - **Type**: [Unpaired preference](https://huggingface.co/docs/trl/main/dataset_formats#unpaired-preference) Column: - `"prompt"`: The input question or instruction provided to the model. - `"completion"`: The model's response to the prompt. - `"label"`: A binary value indicating whether the response is sufficiently helpful. ## Generation script The script used to generate this dataset can be found [here](https://github.com/huggingface/trl/blob/main/examples/datasets/ultrafeedback.py). """) if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] dataset = load_dataset("openbmb/UltraFeedback", split="train") dataset = dataset.filter( lambda example: script_args.model_name in example["models"], batched=False, num_proc=script_args.dataset_num_proc, ) dataset = dataset.map( to_unpaired_preference, remove_columns=["source", "instruction", "models", "completions", "correct_answers", "incorrect_answers"], fn_kwargs={"model_name": script_args.model_name, "aspect": script_args.aspect}, num_proc=script_args.dataset_num_proc, ) dataset = dataset.train_test_split(test_size=0.05, seed=42) if script_args.push_to_hub: dataset.push_to_hub(script_args.repo_id) model_card.push_to_hub(script_args.repo_id, repo_type="dataset") ================================================ FILE: examples/notebooks/README.md ================================================ # Notebooks This directory contains a collection of Jupyter notebooks that demonstrate how to use the TRL library in different applications. | Notebook | Description | Open in Colab | | --- | --- | --- | | [`grpo_trl_lora_qlora.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_trl_lora_qlora.ipynb) | GRPO using QLoRA on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_trl_lora_qlora.ipynb) | | [`grpo_functiongemma_browsergym_openenv.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb) | GRPO on FunctionGemma in the BrowserGym environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb) | | [`grpo_agent.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_agent.ipynb) | GRPO for agent training | Not available due to OOM with Colab GPUs | | [`grpo_rnj_1_instruct.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_rnj_1_instruct.ipynb) | GRPO rnj-1-instruct with QLoRA using TRL on Colab to add reasoning capabilities | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_rnj_1_instruct.ipynb) | | [`sft_ministral3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_ministral3_vl.ipynb) | Supervised Fine-Tuning (SFT) Ministral 3 with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_ministral3_vl.ipynb) | | [`grpo_ministral3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_ministral3_vl.ipynb) | GRPO Ministral 3 with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_ministral3_vl.ipynb) | | [`openenv_sudoku_grpo.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/openenv_sudoku_grpo.ipynb) | GRPO to play Sudoku on an OpenEnv environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb) | | [`openenv_wordle_grpo.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/openenv_wordle_grpo.ipynb) | GRPO to play Worldle on an OpenEnv environment | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/openenv_wordle_grpo.ipynb) | | [`sft_nemotron_3.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_nemotron_3.ipynb) | SFT with LoRA on NVIDIA Nemotron 3 models | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_nemotron_3.ipynb) | | [`sft_trl_lora_qlora.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_trl_lora_qlora.ipynb) | Supervised Fine-Tuning (SFT) using QLoRA on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_trl_lora_qlora.ipynb) | | [`sft_qwen_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_qwen_vl.ipynb) | Supervised Fine-Tuning (SFT) Qwen3-VL with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_qwen_vl.ipynb) | | [`sft_tool_calling.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/sft_tool_calling.ipynb) | Teaching tool calling to a model without native tool-calling support using SFT with QLoRA | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_tool_calling.ipynb) | | [`grpo_qwen3_vl.ipynb`](https://github.com/huggingface/trl/tree/main/examples/notebooks/grpo_qwen3_vl.ipynb) | GRPO Qwen3-VL with QLoRA using TRL on free Colab | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_qwen3_vl.ipynb) | ================================================ FILE: examples/notebooks/grpo_agent.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "id": "63ceecbc-87ad-4ad3-a317-f49267ffc93b", "metadata": {}, "source": [ "# Agent Training with GRPO using TRL\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "\n", "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can train a language model to act as an **agent**. One that learns to reason, interact with external tools, and improve through reinforcement.\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)\n", "- [OpenEnv](https://github.com/meta-pytorch/OpenEnv)\n", "\n", "\n", "TRL supports training agents that can use external tools as part of their decision process. \n", "In this notebook, the agent has access to the **BioGRID database**, which it can query using **read-only SQL commands** to retrieve biological interaction data. The model learns when and how to use tools based on rewards.\n", "\n", "We'll fine-tune a model using GRPO (Group Relative Policy Optimization) via TRL. The agent will:\n", "\n", "1. Generate tool call to query the database if needed.\n", "2. Receive the tool response and add it it to the context.\n", "3. Learn to improve its tool usage and general capabilities over time through reward signals.\n", "\n", "## Install dependencies\n", "\n", "We'll start by installing **TRL**, which automatically includes the main dependencies like **Transformers**. \n", "We'll also install **trackio** (for logging and monitoring training runs), **vLLM** (for efficient generation), and **jmespath** (needed for the tools capabilities)." ] }, { "cell_type": "code", "execution_count": null, "id": "b4812fbf-3f61-481e-9a64-95277eada9c9", "metadata": {}, "outputs": [], "source": [ "!pip install -Uq \"trl[vllm]\" git+https://github.com/huggingface/transformers.git trackio jmespath " ] }, { "cell_type": "markdown", "id": "ede8e566-a1b5-460f-9fe8-a6010bc56148", "metadata": {}, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "id": "21756ac0-78b2-495d-8137-28dfa9faae6a", "metadata": {}, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "id": "KVGklspLYlmz", "metadata": {}, "source": [ "## Create the database for the tool\n", "\n", "For this example, we will use the [BioGRID database](https://thebiogrid.org/), a curated resource containing **protein, genetic, and chemical interaction data**. We've already compiled and uploaded it to the Hub at [qgallouedec/biogrid](https://huggingface.co/datasets/qgallouedec/biogrid). The dataset is loaded and converted into an sqlite database.\n", "\n", "> 💡 We remove spaces in the column names to easen the model work. In real-world deployments, you may keep your original column names and rely on the agent to reason about them. Here, we simplify the schema to make training smoother." ] }, { "cell_type": "code", "execution_count": null, "id": "rRzPMhfXBLkF", "metadata": {}, "outputs": [], "source": [ "import sqlite3\n", "from datasets import load_dataset\n", "\n", "# Load dataset\n", "biogrid_dataset = load_dataset(\"qgallouedec/biogrid\", split=\"train\")\n", "df = biogrid_dataset.to_pandas()\n", "\n", "# Normalize column names: remove spaces, replace with underscores\n", "df.columns = [c.replace(\" \", \"_\") for c in df.columns]\n", "\n", "# Save to SQLite\n", "conn = sqlite3.connect(\"biogrid.db\")\n", "try:\n", " df.to_sql(\"interactions\", conn, if_exists=\"replace\", index=False)\n", " print(f\"biogrid.db created. Rows stored: {len(df)}\")\n", "finally:\n", " conn.close()" ] }, { "cell_type": "markdown", "id": "pSSGvLbmZyC2", "metadata": {}, "source": [ "## Load the QA dataset\n", "\n", "The training objective is to fine-tune a model to answer gene-related questions. The model should learn to use the database query tool to retrieve factual information when needed.\n", "\n", "We'll define a formatting function for each sample, adding instructions about the database and how to call it. The model must answer with **yes** or **no**. Let's implement the `format_example` function.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "asrv7LbaD71C", "metadata": {}, "outputs": [], "source": [ "import textwrap\n", "\n", "def format_example(example):\n", " question = example[\"question\"]\n", " preamble = textwrap.dedent(\"\"\"\\\n", " You have access to the BioGRID SQLite database.\n", " Use SQL queries to retrieve only the information needed to answer the question.\n", "\n", " Genes may appear in the database in columns `Alt_IDs_Interactor_A` `Alt_IDs_Interactor_B`, `Aliases_Interactor_A` and `Aliases_Interactor_B`,\n", " and each entry can contain multiple gene names or synonyms separated by '|', for example:\n", " 'entrez gene/locuslink:JNKK(gene name synonym)|entrez gene/locuslink:MAPKK4(gene name synonym)|...'\n", " So a gene like 'JNKK' or 'MAPKK4' may appear inside one of these strings.\n", "\n", " If the database schema is unclear or you are unsure about column names:\n", " - First inspect the schema with `PRAGMA table_info(interactions);`\n", " - Or preview a few rows with `SELECT * FROM interactions LIMIT 1;`\n", "\n", " Otherwise, directly query the required data.\n", "\n", " Final answer must be enclosed in stars, e.g. *Yes* or *No*.\n", " Facts:\n", " - The NCBI Taxonomy identifier for humans is taxid:9606.\n", " \"\"\")\n", " content = f\"{preamble}\\nQuestion: {question}\"\n", " prompt = [{\"role\": \"user\", \"content\": content}]\n", " return {\"prompt\": prompt}" ] }, { "cell_type": "markdown", "id": "UMnHXYZla_EO", "metadata": {}, "source": [ "Now, let's load the database and call the previous function. \n", "For simplicity, we will only use questions that start with **“Does the gene…”**. \n", "In a real use case, the full dataset can be used.\n", "\n", "The QA dataset is available on the [Hub](https://huggingface.co/datasets/qgallouedec/biogrid_qa)." ] }, { "cell_type": "code", "execution_count": null, "id": "jEs12KqwDnVl", "metadata": {}, "outputs": [], "source": [ "dataset = load_dataset(\"qgallouedec/biogrid_qa\", split=\"train\")\n", "dataset = dataset.filter(\n", " lambda example: example[\"question\"].startswith(\"Does the gene \")\n", ") # keep only simple questions for example\n", "dataset = dataset.map(format_example, remove_columns=[\"question\"])\n", "\n", "train_dataset = dataset\n", "eval_dataset = None # No eval by default, can be added if needed" ] }, { "cell_type": "markdown", "id": "m4GRjbHycM5L", "metadata": {}, "source": [ "## Create tool for the agent\n", "\n", "The `query_biogrid` function is the tool the model will use to query the database and retrieve factual information. \n", "Each tool must be a standard Python function with **type-hinted arguments and return types**, and a **Google-style docstring** describing its purpose, parameters, and return value." ] }, { "cell_type": "code", "execution_count": null, "id": "nLMH7hahGTyO", "metadata": {}, "outputs": [], "source": [ "from contextlib import contextmanager\n", "import signal\n", "\n", "@contextmanager\n", "def timeout(seconds):\n", " \"\"\"Context manager that raises TimeoutError if execution exceeds time limit.\"\"\"\n", "\n", " def timeout_handler(signum, frame):\n", " raise TimeoutError(f\"Operation timed out after {seconds} seconds\")\n", "\n", " signal.signal(signal.SIGALRM, timeout_handler)\n", " signal.alarm(seconds)\n", " try:\n", " yield\n", " finally:\n", " signal.alarm(0)\n", "\n", "def query_biogrid(sql_command: str) -> list[tuple]:\n", " \"\"\"\n", " Execute a read-only SQL command on the BioGRID database.\n", "\n", " BioGRID is a curated biological database that compiles protein, genetic, and chemical interactions from multiple organisms. It provides researchers with experimentally verified interaction data to support studies in systems biology and functional genomics.\n", "\n", " Args:\n", " sql_command: The SQL command to execute.\n", "\n", " Returns:\n", " A list of tuples containing the query results.\n", " \"\"\"\n", " with timeout(5):\n", " conn = sqlite3.connect(\"file:biogrid.db?mode=ro\", uri=True)\n", " cursor = conn.cursor()\n", " try:\n", " cursor.execute(sql_command)\n", " results = cursor.fetchall()\n", " finally:\n", " conn.close()\n", " return results" ] }, { "cell_type": "markdown", "id": "GiHtooTwci3B", "metadata": {}, "source": [ "## Define reward functions\n", "\n", "To guide the agent during training, we define a few simple reward functions:\n", "\n", "- **`query_reward`**: evaluates the model’s query strategy — penalizes more than two queries, penalizes generic database scans, and rewards use of `WHERE` and evidence supporting the final answer.\n", "- **`correctness_reward`**: rewards Yes/No predictions that match the expected answer.\n", "- **`structure_reward`**: rewards a proper assistant structure (tool call → response → optional explanation).\n", "\n", "Each function returns a list of floats used by the **GRPOTrainer** during optimization. \n", "Combined, they encourage effective tool use and factual answers." ] }, { "cell_type": "code", "execution_count": null, "id": "sXyqC6cJGe3L", "metadata": {}, "outputs": [], "source": [ "import re\n", "\n", "def query_reward(completions, answer, **kwargs):\n", " \"\"\"\n", " Reward query strategy:\n", " - Penalize more than 2 queries\n", " - Penalize generic queries (LIMIT 1 / PRAGMA)\n", " - Reward usage of WHERE\n", " - Reward evidence supporting the final answer\n", " \"\"\"\n", " rewards = []\n", "\n", " for completion, ans in zip(completions, answer, strict=False):\n", " reward = 0.0\n", " sql_queries = []\n", " tool_results = []\n", "\n", " # collect all SQL queries and tool results\n", " for turn in completion:\n", " if turn.get(\"tool_calls\"):\n", " for call in turn[\"tool_calls\"]:\n", " sql = call[\"function\"][\"arguments\"].get(\"sql_command\", \"\").lower()\n", " sql_queries.append(sql)\n", " if turn.get(\"role\") == \"tool\" and turn.get(\"content\"):\n", " tool_results.append(turn[\"content\"])\n", "\n", " # --- penalize too many queries ---\n", " if len(sql_queries) > 3:\n", " reward -= 1.5\n", "\n", " # --- check query quality ---\n", " where_count = 0\n", " for q in sql_queries:\n", " if \"limit 1\" in q:\n", " reward -= 1.0\n", " if \" where \" not in q:\n", " reward -= 0.5\n", " else:\n", " where_count += 1\n", " reward += min(where_count, 3) * 0.4 # small bonus for WHERE usage\n", "\n", " # --- evidence check: do queries support the answer? ---\n", " combined_results = []\n", " error_detected = False\n", "\n", " for res in tool_results:\n", " if isinstance(res, dict) and \"error\" in res:\n", " error_detected = True\n", " elif isinstance(res, list):\n", " combined_results.extend(res)\n", "\n", " # if error detected, penalize heavily\n", " if error_detected:\n", " reward -= 2.0\n", " elif len(sql_queries) == 0:\n", " reward -= 1.5\n", " else:\n", " has_hits = len(combined_results) > 0\n", " correct_answer = ans.lower()\n", " if (has_hits and correct_answer == \"yes\") or (not has_hits and correct_answer == \"no\"):\n", " reward += 2.0\n", " else:\n", " reward -= 1.5\n", "\n", " rewards.append(reward)\n", "\n", " return rewards\n", "\n", "\n", "def correctness_reward(completions, answer, **kwargs):\n", " \"\"\"\n", " Reward Yes/No correctness.\n", " Model must provide final answer enclosed in stars — *yes* or *no*.\n", " Does not reward informal yes/no buried in text.\n", " \"\"\"\n", " rewards = []\n", " for completion, ans in zip(completions, answer, strict=False):\n", " raw = completion[-1][\"content\"].lower()\n", "\n", " # detect form *yes* or *no*\n", " match = re.search(r\"\\*(yes|no)\\*\", raw)\n", " guess = match.group(1) if match else None\n", "\n", " reward = 0.0\n", "\n", " if guess is None:\n", " reward -= 0.5 # invalid format\n", " elif guess == ans.lower():\n", " reward += 0.6 # correct under required format\n", " else:\n", " reward -= 1.0 # wrong answer\n", "\n", " rewards.append(reward)\n", "\n", " return rewards\n", "\n", "\n", "def structure_reward(completions, **kwargs):\n", " \"\"\"\n", " Reward proper assistant structure.\n", " Encourages a logical sequence: tool call + response + optional extra content.\n", " \"\"\"\n", " rewards = []\n", "\n", " for completion in completions:\n", " has_call = False\n", " has_response = False\n", " has_other = False\n", "\n", " for turn in completion:\n", " role = turn.get(\"role\")\n", " if role == \"assistant\" and turn.get(\"tool_calls\"):\n", " has_call = True\n", " elif role == \"tool\":\n", " has_response = True\n", " else:\n", " content = turn.get(\"content\")\n", " if content and content.strip() not in [\"\", \"\"]:\n", " has_other = True\n", "\n", " # Reward sequences\n", " if has_call and has_response:\n", " if has_other:\n", " reward = 0.1\n", " else:\n", " reward = 0.05 # still positive even without extra text\n", " elif has_call and not has_response:\n", " reward = -0.15\n", " else:\n", " reward = 0.0 # neutral if no call\n", "\n", " rewards.append(reward)\n", "\n", " return rewards\n" ] }, { "cell_type": "markdown", "id": "zcgkrKtTb4T9", "metadata": {}, "source": [ "## Set GRPO Config\n", "\n", "Next, we define the **GRPOConfig**, which controls the main training parameters. \n", "This configuration specifies how the model interacts with **vLLM**, manages memory, and logs results." ] }, { "cell_type": "code", "execution_count": null, "id": "t4ifJsNLElIN", "metadata": {}, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"grpo_biogrid_qwen_3g-1.7b\"\n", "\n", "grpo_config = GRPOConfig(\n", " # Training schedule / optimization\n", " max_steps=400, # Max number of training steps\n", " chat_template_kwargs = {\"enable_thinking\": False}, # Disable thinking to reduce token generation\n", "\n", " # GRPO configuration\n", " max_completion_length = 1024, # Maximum tokens generated per model response\n", "\n", " # vLLM configuration\n", " use_vllm = True, # Enable vLLM for faster inference during rollouts\n", " vllm_mode = \"colocate\", # Run vLLM in colocate mode (same process as training)\n", " vllm_enable_sleep_mode=False,\n", "\n", " # Logging / reporting\n", " output_dir = output_dir, # Directory for checkpoints and logs\n", " report_to=\"trackio\", # Experiment tracking tool (integrates with HF Spaces)\n", " trackio_space_id = output_dir, # HF Space where experiment tracking will be saved\n", " save_steps = 10, # Interval for saving checkpoints\n", " log_completions = True,\n", "\n", " # Hub integration\n", " push_to_hub = True, # Set True to automatically push model to Hugging Face Hub\n", ")" ] }, { "cell_type": "markdown", "id": "34I-Q2MJuf42", "metadata": {}, "source": [ "## Create `GRPOTrainer` and Start Training\n", "\n", "Next, we initialize the **`GRPOTrainer`**, which handles the full reinforcement learning loop.\n", "\n", "It receives the model name, reward functions, tool(s), and dataset defined earlier. \n", "\n", "Finally, we call `trainer.train()` to begin fine-tuning, allowing the model to learn how to query the database effectively through iterative feedback." ] }, { "cell_type": "code", "execution_count": null, "id": "IysntAUOFvRn", "metadata": {}, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "model_name=\"Qwen/Qwen3-1.7B\"\n", "\n", "trainer = GRPOTrainer(\n", " model=model_name,\n", " train_dataset=train_dataset,\n", " eval_dataset=eval_dataset,\n", " tools=[query_biogrid],\n", " reward_funcs=[correctness_reward, structure_reward, query_reward],\n", " args=grpo_config,\n", ")" ] }, { "cell_type": "markdown", "id": "r_qJ5UwLuzCG", "metadata": {}, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "id": "DusT8JUaGmA6", "metadata": {}, "outputs": [], "source": [ "import torch\n", "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "id": "OTPkiz3fu0lp", "metadata": {}, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "id": "NwI3buPOFMFk", "metadata": {}, "outputs": [], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "id": "ITnLBLcTu2-p", "metadata": {}, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "id": "ftek6m4-GncK", "metadata": {}, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "id": "O6LAwznKu7mc", "metadata": {}, "source": [ "Let's save the trained model." ] }, { "cell_type": "code", "execution_count": null, "id": "idVgnNS1MWPr", "metadata": {}, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub()" ] }, { "cell_type": "markdown", "id": "707318cb", "metadata": {}, "source": [ "## Load the fine-tuned model and run inference using `smolagents`\n", "\n", "After fine-tuning the model with **GRPO (TRL)** for tool calling, we can test it at inference time using **`smolagents`**, a lightweight library for running multi-step agents.\n", "\n", "`smolagents` handles the agent loop for us:\n", "- Detecting tool calls generated by the model\n", "- Executing the corresponding tools (e.g. database queries)\n", "- Feeding the results back to the model until a final answer is produced\n", "\n", "> **Note** \n", "> Using an agent framework is optional. The fine-tuned model can also be used directly with `transformers` by manually controlling the inference loop and executing the tools outside the model.\n", "> Agent frameworks are especially useful when the number of steps or tool calls is not fixed.\n", "\n", "We start by installing the required package:\n" ] }, { "cell_type": "code", "execution_count": null, "id": "aab7fd5c", "metadata": {}, "outputs": [], "source": [ "!pip install git+https://github.com/huggingface/smolagents.git" ] }, { "cell_type": "markdown", "id": "24453572", "metadata": {}, "source": [ "We will use the `CodeAgent` class from `smolagents` to instantiate our agent. \n", "First, we need to define the tool the agent can use. This is done using the `@tool` decorator.\n", "\n", "As shown below, the tool definition is **exactly the same** as the one used during GRPO training with TRL. This consistency is important: the model was trained to emit calls following this schema, and at inference time the agent simply executes the corresponding Python function." ] }, { "cell_type": "code", "execution_count": null, "id": "adcbbafa", "metadata": {}, "outputs": [], "source": [ "from smolagents import tool\n", "\n", "@tool\n", "def query_biogrid(sql_command: str) -> list[tuple]:\n", " \"\"\"\n", " Execute a read-only SQL query on the BioGRID database.\n", "\n", " BioGRID is a curated biological database that compiles protein, genetic,\n", " and chemical interactions from multiple organisms.\n", "\n", " Args:\n", " sql_command: A read-only SQL query to execute.\n", "\n", " Returns:\n", " A list of tuples containing the query results.\n", " \"\"\"\n", " with timeout(5):\n", " conn = sqlite3.connect(\n", " \"file:biogrid.db?mode=ro\",\n", " uri=True,\n", " )\n", " cursor = conn.cursor()\n", " try:\n", " cursor.execute(sql_command)\n", " results = cursor.fetchall()\n", " finally:\n", " conn.close()\n", "\n", " return results" ] }, { "cell_type": "markdown", "id": "59721ad2", "metadata": {}, "source": [ "Now we can instantiate the agent using our fine-tuned model and the database tool defined above.\n", "We wrap the model with `TransformersModel` and pass both the model and the tool when creating the `CodeAgent`." ] }, { "cell_type": "code", "execution_count": null, "id": "e9ed8d00", "metadata": {}, "outputs": [], "source": [ "from smolagents import TransformersModel, CodeAgent\n", "\n", "model = TransformersModel(model_id=\"sergiopaniego/grpo_biogrid_qwen_3g-1.7b\", apply_chat_template_kwargs={\"enable_thinking\": False})\n", "\n", "# Create an agent with query_biogrid as tool\n", "agent = CodeAgent(tools=[query_biogrid], model=model)" ] }, { "cell_type": "markdown", "id": "57ba9462", "metadata": {}, "source": [ "Finally, we run the agent by passing the full prompt (including the instruction preamble and the question), exactly as it was used during training. This ensures the agent operates under the same context and assumptions learned with GRPO, allowing it to correctly decide when to query the database and how to format the final answer." ] }, { "cell_type": "code", "execution_count": null, "id": "23a3cdf4", "metadata": {}, "outputs": [], "source": [ "result = agent.run(train_dataset[0]['prompt'][0]['content'])\n", "print(result)" ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "lSR2nwdJg962" }, "source": [ "# Fine-Tune FunctionGemma using Hugging Face TRL and OpenEnv\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_functiongemma_browsergym_openenv.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "This guide describes the process of fine-tuning [FunctionGemma](https://huggingface.co/google/functiongemma-270m-it) by Google DeepMind in the [BrowserGym](https://meta-pytorch.org/OpenEnv/environments/browsergym/) environment provided by OpenEnv, using Hugging Face TRL. The steps covered include:\n", "\n", "* What is GRPO and OpenEnv\n", "* Setup dependencies for training\n", "* Initialize the OpenEnv's BrowserGym environment\n", "* Create rollout function with helpers\n", "* Define the reward functions\n", "* Load the custom dataset\n", "* Fine tune using TRL and the GRPOTrainer\n", "* Load the fine-tuned model and run inference\n", "\n", "> Note: The guide is designed to run on Google Colaboratory with access to an NVIDIA A100 GPU (40GB) using FunctionGemma. The workflow can be adapted to other GPU configurations, models, or environments." ] }, { "cell_type": "markdown", "metadata": { "id": "duXYuR6Cu_na" }, "source": [ "## What is GRPO and OpenEnv\n", "\n", "Group Relative Policy Optimization ([GRPO](https://huggingface.co/papers/2402.03300)) is a post-training method widely used for efficiently fine-tuning large language models. GRPO leverages reward functions to guide learning, enabling models to optimize task-specific behaviors without retraining the entire network.\n", "\n", "[OpenEnv](https://meta-pytorch.org/OpenEnv) provides a standard interface for interacting with agentic execution environments using simple Gymnasium-style APIs, such as `step()`, `reset()`, and `state()`. These APIs facilitate reinforcement learning training loops by allowing models to interact with environments in a structured manner. OpenEnv also offers tools for environment creators to build isolated, secure, and deployable environments that can be shared via common protocols like HTTP or packaged in Docker.\n", "\n", "The combination of GRPO and OpenEnv enables efficient fine-tuning of models in controlled, interactive tasks while minimizing resource requirements." ] }, { "cell_type": "markdown", "metadata": { "id": "cpSAQkzKmv50" }, "source": [ "## Setup dependencies for training\n", "\n", "Install the required libraries, including Hugging Face TRL for fine-tuning and OpenEnv for reinforcement learning environments." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "c-2drnj5BP56" }, "outputs": [], "source": [ "!pip install -Uq trl[vllm] git+https://huggingface.co/spaces/openenv/browsergym_env liger-kernel trackio" ] }, { "cell_type": "markdown", "metadata": { "id": "Inxeq6ZGpRno" }, "source": [ "A valid Hugging Face token is required to save the fine-tuned model. In Google Colab, the token can be securely accessed through Colab secrets. Otherwise, it can be provided directly in the login method. Ensure the token has write permissions to allow uploading the model to the Hugging Face Hub during training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "C4q5UVu3BP57" }, "outputs": [], "source": [ "from google.colab import userdata\n", "from huggingface_hub import login\n", "\n", "# Login into Hugging Face Hub\n", "hf_token = userdata.get('HF_TOKEN') # If you are running inside a Google Colab\n", "login(hf_token)" ] }, { "cell_type": "markdown", "metadata": { "id": "O3kr38TGm_hb" }, "source": [ "## Initialize the OpenEnv's BrowserGym environment\n", "\n", "External environments can guide the fine-tuning of LLMs for function calling by providing interactive feedback that enhances performance on task-specific behaviors.\n", "\n", "[BrowserGym](https://meta-pytorch.org/OpenEnv/environments/browsergym/) is a unified framework for web-based agent tasks, offering multiple benchmarks through a Gymnasium-compatible API. It enables training on simple synthetic tasks with [MiniWoB++](https://github.com/Farama-Foundation/miniwob-plusplus) and evaluation on more complex, realistic tasks with [WebArena](https://github.com/web-arena-x/webarena), [VisualWebArena](https://github.com/web-arena-x/visualwebarena), or [WorkArena](https://github.com/ServiceNow/WorkArena). This setup supports iterative training and assessment of web agents without requiring extensive infrastructure.\n", "\n", "BrowserGym supports both LLM and VLM training by providing visual information, including screenshots and DOM data, which can be utilized depending on the model type. This guide focuses on a simple web-based task called *\"click-test\"*, which is part of the MiniWoB++ benchmark of synthetic web tasks. Environments can be run locally, in Docker containers, or accessed remotely via the Hugging Face Hub. For this example, the remote environment [openenv/browsergym_env](https://huggingface.co/spaces/openenv/browsergym_env) will be used.\n", "\n", "> Note: Hosted environments on the Hub currently have limited concurrency. For higher reliability or parallel runs, duplicating the Space to your own account is strongly recommended." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "clDs-WQlBP57" }, "outputs": [], "source": [ "from browsergym_env import BrowserGymEnv\n", "space_url = \"https://openenv-browsergym-env.hf.space\"\n", "\n", "client = BrowserGymEnv(base_url=space_url)" ] }, { "cell_type": "markdown", "metadata": { "id": "EqfDavDQnD_5" }, "source": [ "## Create rollout function with helpers\n", "\n", "The rollout function defines how the agent interacts with the environment during GRPO training. It generates model outputs, collects feedback in the form of rewards, and returns the information required for optimization.\n", "\n", "In this setup:\n", "- The function is invoked automatically by the GRPOTrainer (introduced later), which orchestrates the training loop and handles policy updates.\n", "- It uses the trainer's `generate_rollout_completions()` method for efficient output generation. This leverages vLLM, a high-performance inference engine for large language models, and is integrated within TRL to streamline rollout generation and reward collection during fine-tuning.\n", "- Each rollout represents a complete interaction loop, where the model acts, receives feedback from the environment, and updates based on reward signals.\n", "\n", "Rewards capture various aspects of the agent's performance. Helper functions, such as `rollout_once`, manage individual episodes, keeping the main `rollout_func` clean, modular, and reusable.\n", "\n", "This modular structure allows GRPO to efficiently sample, evaluate, and refine the model's behavior through reinforcement learning.\n", "\n", "Before executing rollouts, a `system prompt` is defined to instruct the model on how to interact with the environment. This prompt specifies the available BrowserGym actions (such as `click`, `fill`, `send_keys`, and `scroll`), describes the page structure, and enforces that the model responds with exactly one action per step. It ensures consistent and structured interactions, guiding the model to complete tasks effectively without providing extra explanations or multiple actions." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ItCXS6H0BP58" }, "outputs": [], "source": [ "# @title System prompt (click to expand)\n", "SYSTEM_PROMPT = \"\"\"You control a web browser through BrowserGym actions.\n", "You must complete the given web task by interacting with the page.\n", "\n", "Available actions:\n", "- noop() - Do nothing\n", "- click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "- fill(bid, text) - Fill input field with text\n", "- send_keys(text) - Send keyboard input\n", "- scroll(direction) - Scroll up/down\n", "\n", "The page structure shows elements as: [bid] element_type 'element_text'\n", "For example: [13] button 'Click Me!' means bid='13'\n", "\n", "Reply with exactly ONE action on a single line, e.g.:\n", "click('13')\n", "fill('42', 'hello world')\n", "noop()\n", "\n", "Do not include explanations or multiple actions.\"\"\"" ] }, { "cell_type": "markdown", "metadata": { "id": "Vi1rFey39GUl" }, "source": [ "The `rollout_func` orchestrates the interaction between the model and the remote BrowserGym environment. For each prompt in the batch, it executes a complete episode using the `rollout_once` function, collecting model outputs and rewards for GRPO optimization.\n", "\n", "The parameter `max_steps` defines the maximum number of steps the model can take within a single episode. This limits the length of the interaction loop, ensuring that episodes terminate even if the task is not completed, and helps maintain efficient training.\n", "\n", "During each episode, the function tracks prompt and completion IDs, log probabilities, and both step-wise and final rewards, returning them in a structured format for the trainer to perform policy updates." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "CgHd5CFBBP58" }, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "max_steps=10\n", "\n", "def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]:\n", " episode_prompt_ids: list[list[int]] = []\n", " episode_completion_ids: list[list[int]] = []\n", " episode_logprobs: list[list[float]] = []\n", " completion_rewards: list[float] = []\n", "\n", " print(f\"\\n[DEBUG] rollout_func called with {len(prompts)} prompts (LLM mode, text-only)\")\n", "\n", " for i, prompt_text in enumerate(prompts):\n", " print(f\"[DEBUG] Processing prompt {i + 1}/{len(prompts)}\")\n", " episode = rollout_once(\n", " trainer=trainer,\n", " env=client,\n", " tokenizer=trainer.processing_class,\n", " dataset_prompt=prompt_text,\n", " max_steps=max_steps,\n", " )\n", " episode_prompt_ids.append(episode[\"prompt_ids\"])\n", " episode_completion_ids.append(episode[\"completion_ids\"])\n", " episode_logprobs.append(episode[\"logprobs\"])\n", " completion_rewards.append(episode[\"completion_reward\"])\n", "\n", " return {\n", " \"prompt_ids\": episode_prompt_ids,\n", " \"completion_ids\": episode_completion_ids,\n", " \"logprobs\": episode_logprobs,\n", " \"completion_reward\": completion_rewards,\n", " }" ] }, { "cell_type": "markdown", "metadata": { "id": "ioUHdIxr9ZQO" }, "source": [ "### Define `rollout_once`\n", "\n", "The `rollout_once` function runs one complete interaction loop between the model and the BrowserGym environment using the trainer's generation method. \n", "It executes a single episode, from generating an action to receiving feedback and computing rewards.\n", "\n", "Here's the step-by-step breakdown:\n", "\n", "1. Environment reset: Start a new BrowserGym session and initialize the observation.\n", "2. Prompt construction: Combine the system prompt, environment observation (text-only via the accessibility tree), and any relevant errors or state information to form the model input.\n", "3. Generation: Use `trl.experimental.openenv.generate_rollout_completions()` to produce the model's action efficiently with vLLM.\n", "4. Action parsing and execution: Interpret the model's output and execute the corresponding BrowserGym action (e.g., `click`, `fill`, `scroll`).\n", "5. Reward calculation: Track step-wise rewards provided by the environment and compute completion rewards based on task success or failure.\n", "6. Return structured rollout data: Includes prompt/completion IDs, log probabilities, step rewards, and the final reward for the episode.\n", "\n", "This modular design allows each episode to be processed independently while providing rich feedback for the GRPO training loop, supporting both task completion and intermediate reward shaping." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "y8Ml47SYBP58" }, "outputs": [], "source": [ "from trl.experimental.openenv import generate_rollout_completions\n", "from browsergym_env import BrowserGymAction\n", "from transformers import AutoTokenizer\n", "\n", "def rollout_once(\n", " trainer: GRPOTrainer,\n", " env: BrowserGymEnv,\n", " tokenizer: AutoTokenizer,\n", " dataset_prompt: str,\n", " max_steps: int,\n", ") -> dict[str, list]:\n", " \"\"\"Run one episode and collect training data (text-only, no screenshots).\"\"\"\n", " result = env.reset()\n", " observation = result.observation\n", "\n", " prompt_ids: list[int] = []\n", " completion_ids: list[int] = []\n", " logprobs: list[float] = []\n", " step_rewards: list[float] = []\n", " completion_rewards: list[float] = []\n", "\n", " for step_num in range(max_steps):\n", " if result.done:\n", " break\n", "\n", " # Create prompt from observation (text-only using accessibility tree)\n", " goal = observation.goal or dataset_prompt\n", " axtree = observation.axtree_txt or \"\"\n", " error = observation.error if observation.last_action_error else \"\"\n", "\n", " user_prompt = make_user_prompt(goal, step_num, axtree, error)\n", " messages = [\n", " {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n", " {\"role\": \"user\", \"content\": user_prompt},\n", " ]\n", " prompt_text = tokenizer.apply_chat_template(\n", " messages,\n", " add_generation_prompt=True,\n", " tokenize=False,\n", " )\n", "\n", " # Generate action with vLLM\n", " rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0]\n", " prompt_ids.extend(rollout_outputs[\"prompt_ids\"])\n", " completion_ids.extend(rollout_outputs[\"completion_ids\"])\n", " logprobs.extend(rollout_outputs[\"logprobs\"])\n", "\n", " completion_text = rollout_outputs.get(\"text\") or tokenizer.decode(\n", " rollout_outputs[\"completion_ids\"], skip_special_tokens=True\n", " )\n", "\n", " # Parse and execute action\n", " action_str = parse_action(completion_text)\n", "\n", " print(f\"Step {step_num + 1}: {action_str}\")\n", "\n", " # Take action in environment\n", " result = env.step(BrowserGymAction(action_str=action_str))\n", " observation = result.observation\n", "\n", " # Track rewards\n", " step_reward = float(result.reward or 0.0)\n", " step_rewards.append(step_reward)\n", "\n", " # Reward shaping: success is most important\n", " if result.done and step_reward > 0:\n", " completion_rewards.append(1.0) # Task completed successfully\n", " elif result.done and step_reward == 0:\n", " completion_rewards.append(0.0) # Task failed\n", " else:\n", " completion_rewards.append(step_reward) # Intermediate reward\n", "\n", " # Final reward is based on task completion\n", " final_reward = completion_rewards[-1] if completion_rewards else 0.0\n", "\n", " return {\n", " \"prompt_ids\": prompt_ids,\n", " \"completion_ids\": completion_ids,\n", " \"logprobs\": logprobs,\n", " \"step_rewards\": step_rewards,\n", " \"completion_reward\": final_reward,\n", " }" ] }, { "cell_type": "markdown", "metadata": { "id": "MDJKMQ__8qzj" }, "source": [ "### Helper functions\n", "\n", "Supporting utilities used in `rollout_once`:\n", "\n", "- `make_user_prompt`: builds the user prompt combining the base text and previous game messages.\n", "- `parse_action`: parses BrowserGym action from model response" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "GG4ba41PBP58" }, "outputs": [], "source": [ "# @title Helpers (click to expand)\n", "def make_user_prompt(goal: str, step_num: int, axtree: str, error: str = \"\") -> str:\n", " \"\"\"Create user prompt from observation.\"\"\"\n", " prompt_parts = [f\"Step {step_num + 1}\"]\n", "\n", " if goal:\n", " prompt_parts.append(f\"Goal: {goal}\")\n", "\n", " if error:\n", " prompt_parts.append(f\"Previous action error: {error}\")\n", "\n", " # Include accessibility tree (truncated for context)\n", " if axtree:\n", " max_len = 2000\n", " axtree_truncated = axtree[:max_len] + \"...\" if len(axtree) > max_len else axtree\n", " prompt_parts.append(f\"Page structure:\\n{axtree_truncated}\")\n", "\n", " prompt_parts.append(\"What action do you take?\")\n", "\n", " return \"\\n\\n\".join(prompt_parts)\n", "\n", "\n", "def parse_action(response_text: str) -> str:\n", " \"\"\"Parse BrowserGym action from model response.\"\"\"\n", " # Extract first line that looks like an action\n", " for line in response_text.strip().split(\"\\n\"):\n", " line = line.strip()\n", " if \"(\" in line and \")\" in line:\n", " return line\n", "\n", " # Fallback to noop if no valid action found\n", " return \"noop()\"" ] }, { "cell_type": "markdown", "metadata": { "id": "Oek3JhcWnKhw" }, "source": [ "## Define the reward functions\n", "\n", "Reward functions quantify the model's performance in the environment and guide the GRPO optimization process.\n", "\n", "In this setup, the `reward_completion` function assigns rewards based on task completion. It extracts the final reward for each episode, which indicates whether the agent successfully completed the task. If no reward information is available, it defaults to zero.\n", "\n", "This modular approach allows additional reward functions to be added easily, enabling more granular feedback such as intermediate progress, efficiency, or correctness of actions, depending on the task requirements." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "WxkXaz5aBP59" }, "outputs": [], "source": [ "def reward_completion(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Reward for task completion.\"\"\"\n", " rewards = kwargs.get(\"completion_reward\") if kwargs else None\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]" ] }, { "cell_type": "markdown", "metadata": { "id": "66ZsrLplm07U" }, "source": [ "## Load the custom dataset\n", "\n", "The dataset is constructed with repeated prompts to control the total number of training episodes.\n", "\n", "Each entry in the dataset triggers a single rollout episode during training. The `dataset_prompt` provides the initial instruction to the model at the start of each episode, ensuring consistent guidance for task execution." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "UX6jUjxaBP59" }, "outputs": [], "source": [ "from datasets import Dataset\n", "\n", "dataset_prompt = \"Complete the web task successfully.\"\n", "dataset_size = 1000\n", "\n", "dataset = Dataset.from_dict({\"prompt\": [dataset_prompt] * dataset_size})" ] }, { "cell_type": "markdown", "metadata": { "id": "-mvka-96m3I7" }, "source": [ "## Fine-tune using TRL and the GRPOTrainer\n", "\n", "The next step is to define the GRPOConfig, which sets all key training parameters.\n", "\n", "This configuration determines how the model interacts with vLLM, handles memory and computation, and records training metrics and logs for monitoring the fine-tuning process." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "TZ34a1h-BP59" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "output_dir = \"browsergym-grpo-functiongemma-270m-it\"\n", "\n", "grpo_config = GRPOConfig(\n", " # num_train_epochs=1, # Number of times to iterate over the full dataset (use for full training runs)\n", " max_steps=100, # Number of dataset passes (for shorter runs/testing). For full trainings, use `num_train_epochs` instead\n", " learning_rate=5e-6, # Learning rate for the optimizer\n", " warmup_steps=10, # Number of steps to linearly increase learning rate at the start of training\n", "\n", " per_device_train_batch_size=1, # Number of samples per device per step\n", " num_generations=4, # Number of completions to generate per prompt\n", " generation_batch_size=4, # Batch size used during generation (must be divisible by num_generations)\n", " max_completion_length=32, # Maximum length of generated completions\n", "\n", " use_vllm=True, # Use vLLM engine for fast inference\n", " vllm_mode=\"colocate\", # vLLM mode: \"colocate\" runs generation on the same GPU as training\n", " vllm_gpu_memory_utilization=0.1, # Fraction of GPU memory allocated to vLLM\n", "\n", " output_dir=str(output_dir), # Directory where checkpoints, logs, and outputs will be saved\n", " logging_steps=1, # Log metrics every N steps\n", " report_to=\"trackio\", # Logging/reporting platform (e.g., \"trackio\")\n", " trackio_space_id=output_dir, # HF Space where the experiment tracking will be saved\n", " push_to_hub=True, # Optionally push trained model to Hugging Face Hub\n", "\n", " use_liger_kernel=True, # Enable Liger kernel optimizations for faster training\n", ")\n" ] }, { "cell_type": "markdown", "metadata": { "id": "a1taGmD--0Y4" }, "source": [ "The next step is to initialize the GRPOTrainer, which manages the complete reinforcement learning loop.\n", "\n", "It receives the model name, reward functions, rollout function, and dataset defined earlier. From the model name, the trainer automatically initializes the model and tokenizer. It then coordinates interactions between the model and the environment, applies the defined reward signals, and updates the policy during training.\n", "\n", "Finally, calling `trainer.train()` starts the fine-tuning process, enabling the model to progressively improve its performance through iterative interaction and reinforcement learning.\n", "\n", "> Note: The training pipeline uses approximately 10.6 GB of GPU VRAM and can be adapted to different hardware configurations." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "En43o4NZBP59" }, "outputs": [], "source": [ "model_name = \"google/functiongemma-270m-it\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "047d386e54704add95edd4beace781d7" ] }, "id": "k8-SvqJcBP59", "outputId": "6a4d9276-fc91-4217-d3a2-51a18d222338" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/tmp/ipython-input-3830121904.py:1: UserWarning: You are importing from 'rollout_func', which is an experimental feature. This API may change or be removed at any time without prior notice. Silence this warning by setting environment variable TRL_EXPERIMENTAL_SILENCE=1.\n", " trainer = GRPOTrainer(\n", "The model is already on multiple devices. Skipping the move to device specified in `args`.\n", "`torch_dtype` is deprecated! Use `dtype` instead!\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "047d386e54704add95edd4beace781d7", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading safetensors checkpoint shards: 0% Completed | 0/1 [00:00" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Created new run: sergiopaniego-1765969078\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: Click 'click(bid) - Click element with BrowserGym ID (the number in brackets\n", "Step 8: I will use the action `click()` to click the button.\n", "Step 9: noop()\n", "Step 10: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: Clicks ('13')\n", "Step 4: I will click 'Click Me!' using action 'click(bid)' on page 'Click Test Task' using a bid of '13'.\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: I will use the 'click(bid)' action.\n", "Step 2: mouse_click(bid)\n", "Step 3: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 4: Add action 'click(bid)' to Step 4.\n", "Step 5: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 9: noop()\n", "Step 10: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: Click('13')\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "WARNING:liger_kernel.transformers.model.gemma3:It is strongly recommended to train Gemma3 models with the `eager` attention implementation instead of `sdpa`. Use `eager` with `AutoModelForCausalLM.from_pretrained('', attn_implementation='eager')`.\n", "/usr/local/lib/python3.12/dist-packages/torch/_inductor/compile_fx.py:282: UserWarning: TensorFloat32 tensor cores for float32 matrix multiplication available but not enabled. Consider setting `torch.set_float32_matmul_precision('high')` for better performance.\n", " warnings.warn(\n", "/usr/local/lib/python3.12/dist-packages/torch/_inductor/lowering.py:7095: UserWarning: \n", "Online softmax is disabled on the fly since Inductor decides to\n", "split the reduction. Cut an issue to PyTorch if this is an\n", "important use case and you want to speed it up with online\n", "softmax.\n", "\n", " warnings.warn(\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [100/100 35:02, Epoch 0/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
10.000000
20.000000
30.000000
40.000000
50.000000
60.000000
70.000000
80.000000
9-0.877900
101965.894400
11-0.830900
1210.616100
130.000000
140.000000
150.000000
160.000000
172.320100
181.887500
19-0.691600
20-0.764400
210.000000
220.000000
230.000000
240.000000
250.000000
260.000000
270.000000
280.000000
290.000000
300.000000
310.000000
320.000000
330.000000
340.000000
350.000000
360.000000
370.000000
380.000000
390.000000
400.000000
410.000000
420.000000
430.000000
440.000000
450.000000
460.000000
470.000000
480.000000
490.000000
500.000000
510.000000
520.000000
530.000000
540.000000
550.000000
560.000000
570.000000
580.000000
590.000000
600.000000
610.000000
620.000000
630.000000
640.000000
650.000000
660.000000
670.000000
680.000000
690.000000
700.000000
710.000000
720.000000
730.000000
740.000000
750.000000
760.000000
770.000000
780.000000
790.000000
800.000000
810.000000
820.000000
830.000000
840.000000
850.000000
860.000000
870.000000
880.000000
890.000000
900.000000
910.000000
920.000000
930.000000
940.000000
950.000000
960.000000
970.000000
980.000000
990.000000
1000.000000

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: Clicks ('13')\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: noop()\n", "Step 2: I will use action: click(bid) to click the button.\n", "Step 3: Yes, I can handle this. I will use the `click()` action to click the button.\n", "Step 4: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 9: noop()\n", "Step 10: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 8: noop()\n", "Step 9: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 10: Pass the button ID ('Click Me!') to the action \"click('bid')\".\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: I will click the button by emitting `click(bid)` and `fill(bid, text)` simultaneously.\n", "Step 6: noop()\n", "Step 7: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: - Noop()\n", "Step 2: noop()\n", "Step 3: -noop()\n", "Step 4: noop()\n", "Step 5: Click('13')\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: noop()\n", "Step 2: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: Complete action: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: I will use the action 'click('bid') to click the button.\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: I call action Click (bid) on the page.\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: Oops()\n", "Step 2: noop()\n", "Step 3: fill(bid, text)\n", "Step 4: noop()\n", "Step 5: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: def click_button_on_page():\n", "Step 2: noop()\n", "Step 3: click(bid)\n", "Step 4: Click('13')\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: noop()\n", "Step 2: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 3: noop()\n", "Step 4: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 5: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 6: I will click the button 'Click Me!' by using the action `click(bid)` and emitting a bid of 13.\n", "Step 7: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: noop()\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: `click(bid)` - No action\n", "Step 2: - Noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: noop()\n", "Step 10: I will click the button 'Click Me!' using the action 'click(bid)'.\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: Complete action: click(bid)\n", "Step 10: noop()\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: noop()\n", "Step 2: I will perform action 1: click('13') to complete the action.\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: noop()\n", "Step 2: noop()\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: Click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 8: noop()\n", "Step 9: Click ('13')\n", "Step 10: Add action 'fill(bid, text) - Send keyboard input' to perform the click.\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: noop()\n", "Step 2: Click('click(bid) - Bid')\n", "Step 3: noop()\n", "Step 4: noop()\n", "Step 5: noop()\n", "Step 6: noop()\n", "Step 7: noop()\n", "Step 8: noop()\n", "Step 9: click(bid) - Click element with BrowserGym ID (the number in brackets)\n", "Step 10: noop()\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "\n", "[DEBUG] rollout_func called with 4 prompts (LLM mode, text-only)\n", "[DEBUG] Processing prompt 1/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 2/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 3/4\n", "Step 1: click('13')\n", "[DEBUG] Processing prompt 4/4\n", "Step 1: click('13')\n", "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "BZj4IG9ZBAix" }, "source": [ "In this step, the fine-tuned model is saved locally and uploaded to the Hugging Face Hub using the configured account credentials." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "244ced1920694dbaae9bf98065b4f01d", "e3769ae107554c9ba38c1e491b15bf4e", "6d5b8bff73474faeb1d1b438fb4e8cec", "9f952f8eb63b42e4b38711737da5461e", "bd12780895064467b5be14e2ec3df114", "d1261c1083a74dca877e6eece6395d73", "999744cacd6a4fb08a1d4977ce2f06fd", "faa5e0fb4ee244689c0f9eef9902acf7", "6403bed2cd984ba18f74f416748c64e4", "38be017369524e2eb22050e7a0a18ec5", "b0720a4a2df948308011d4d87a288426", "889ca2520f4d446daf2e6ed16ce11d2e" ] }, "id": "9oOBgEWeBP59", "outputId": "76bef375-fc6b-4fdd-a296-549a9b109b11" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "244ced1920694dbaae9bf98065b4f01d", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e3769ae107554c9ba38c1e491b15bf4e", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "6d5b8bff73474faeb1d1b438fb4e8cec", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...270m-it/training_args.bin: 100%|##########| 7.57kB / 7.57kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "9f952f8eb63b42e4b38711737da5461e", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...a-270m-it/tokenizer.model: 100%|##########| 4.69MB / 4.69MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "bd12780895064467b5be14e2ec3df114", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...ma-270m-it/tokenizer.json: 100%|##########| 33.4MB / 33.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d1261c1083a74dca877e6eece6395d73", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...270m-it/model.safetensors: 4%|3 | 41.9MB / 1.07GB " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "No files have been modified since last commit. Skipping to prevent empty commit.\n", "WARNING:huggingface_hub.hf_api:No files have been modified since last commit. Skipping to prevent empty commit.\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "999744cacd6a4fb08a1d4977ce2f06fd", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "faa5e0fb4ee244689c0f9eef9902acf7", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "6403bed2cd984ba18f74f416748c64e4", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...270m-it/training_args.bin: 100%|##########| 7.57kB / 7.57kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "38be017369524e2eb22050e7a0a18ec5", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...a-270m-it/tokenizer.model: 100%|##########| 4.69MB / 4.69MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b0720a4a2df948308011d4d87a288426", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...270m-it/model.safetensors: 3%|3 | 33.5MB / 1.07GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "889ca2520f4d446daf2e6ed16ce11d2e", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...ma-270m-it/tokenizer.json: 100%|##########| 33.4MB / 33.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "No files have been modified since last commit. Skipping to prevent empty commit.\n", "WARNING:huggingface_hub.hf_api:No files have been modified since last commit. Skipping to prevent empty commit.\n" ] }, { "data": { "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" }, "text/plain": [ "CommitInfo(commit_url='https://huggingface.co/sergiopaniego/browsergym-grpo-functiongemma-270m-it/commit/a17de133c28ca7fddfcb2694c32f2791de5ddbe6', commit_message='End of training', commit_description='', oid='a17de133c28ca7fddfcb2694c32f2791de5ddbe6', pr_url=None, repo_url=RepoUrl('https://huggingface.co/sergiopaniego/browsergym-grpo-functiongemma-270m-it', endpoint='https://huggingface.co', repo_type='model', repo_id='sergiopaniego/browsergym-grpo-functiongemma-270m-it'), pr_revision=None, pr_num=None)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub()" ] }, { "cell_type": "markdown", "metadata": { "id": "talmc8b7nPXJ" }, "source": [ "## Load the Fine-Tuned Model and Run Inference\n", "\n", "The fine-tuned model is loaded to perform inference and evaluate its behavior on the target task. \n", "In this case, the model is tested within the BrowserGym environment using OpenEnv, focusing on the *click* task from the MiniWoB++ benchmark, which is included among the available BrowserGym tasks." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "c3879b716f37442a87d51b8414fe8c48" ] }, "id": "iIDiaGVlBP5-", "outputId": "4dc0e365-e89f-40ba-b391-74c7efdc932d" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "c3879b716f37442a87d51b8414fe8c48", "version_major": 2, "version_minor": 0 }, "text/plain": [ "model.safetensors: 0%| | 0.00/1.07G [00:00 **Note:**\n", "> In older GPUs (including those available on Colab), **FP8 support** is limited, so we use the BF16 version of the model.\n", "> In that case, you can select the official checkpoint or the one from Unsloth.\n", "> If you have access to GPUs with **FP8 support**, you can switch to that version instead." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "83dfeaab2bd04b06899d09b6b35bacd1", "8588996c1d2d444193e9cf53c1a73b8e", "138a997da09f40ada32171e51b51b708", "06ef4d5f41de4436ad4731cbf2f8471f" ] }, "id": "WlK7KYKT99p-", "outputId": "db72808f-21cf-4022-ed1a-b78ebb3ee47e" }, "outputs": [], "source": [ "from transformers import AutoProcessor\n", "\n", "#model_name = \"mistralai/Ministral-3-3B-Instruct-2512\"\n", "model_name = \"mistralai/Ministral-3-3B-Instruct-2512-BF16\" # \"unsloth/Ministral-3-3B-Instruct-2512\"\n", "\n", "processor = AutoProcessor.from_pretrained(model_name, padding_side=\"left\")\n", "\n", "SYSTEM_PROMPT = (\n", " \"You are a helpful AI Assistant that provides well-reasoned and detailed responses. \"\n", " \"You first think about the reasoning process as an internal monologue and then provide the user with the answer. \"\n", " \"Respond in the following format: \\n...\\n\\n\\n...\\n\"\n", ")\n", "\n", "\n", "def make_conversation(example):\n", " conversation = [\n", " {\n", " \"role\": \"system\",\n", " \"content\": [{\"type\": \"text\", \"text\": SYSTEM_PROMPT}],\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\"type\": \"image\", \"image\": example[\"image\"]},\n", " {\"type\": \"text\", \"text\": example[\"problem\"]},\n", " ],\n", " },\n", " ]\n", " return {\n", " \"prompt\": conversation,\n", " \"image\": example[\"image\"],\n", " }\n", "\n", "train_dataset = train_dataset.map(make_conversation)" ] }, { "cell_type": "markdown", "metadata": { "id": "5txAuMAa8ock" }, "source": [ "Let's review one example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "sjxG7duU99p_" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ZooycTF099p_" }, "outputs": [], "source": [ "train_dataset = train_dataset.remove_columns(['problem', 'original_question', 'original_answer'])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "2LcjFKgD99p_" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "markdown", "metadata": { "id": "YY3uMp909Eqy" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "RcQn7mGs99p_" }, "outputs": [], "source": [ "from transformers import Mistral3ForConditionalGeneration, FineGrainedFP8Config, BitsAndBytesConfig\n", "import torch\n", "\n", "FP8 = False\n", "\n", "if FP8:\n", " model_name = \"mistralai/Ministral-3-3B-Instruct-2512\"\n", " quantization_config = FineGrainedFP8Config(dequantize=False)\n", "else:\n", " model_name = \"mistralai/Ministral-3-3B-Instruct-2512-BF16\" # \"unsloth/Ministral-3-3B-Instruct-2512\"\n", " quantization_config = BitsAndBytesConfig(\n", " load_in_4bit=True, # Load the model in 4-bit precision to save memory\n", " bnb_4bit_compute_dtype=torch.float16, # Data type used for internal computations in quantization\n", " bnb_4bit_use_double_quant=True, # Use double quantization to improve accuracy\n", " bnb_4bit_quant_type=\"nf4\", # Type of quantization. \"nf4\" is recommended for recent LLMs\n", " )\n", "\n", "model = Mistral3ForConditionalGeneration.from_pretrained(\n", " model_name,\n", " dtype=\"float32\",\n", " device_map=\"auto\",\n", " quantization_config=quantization_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "WZGf-GF09Gsc" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter** — a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LqCEI4hf99p_" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different VLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=8,\n", " lora_alpha=32,\n", " lora_dropout=0.1,\n", " target_modules=[\"q_proj\", \"v_proj\"],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "mDq4V6dN9MGk" }, "source": [ "## Train model\n", "\n", "We'll configure **GRPO** using `GRPOConfig`, keeping the parameters minimal so the training fits on a free Colab instance. You can adjust these settings if more resources are available. For full details on all available parameters, check the [TRL GRPOConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.GRPOConfig).\n", "\n", "First, we need to define the rewards functions that the training algorithm will use to improve the model. In this case, we'll include two reward functions.\n", "We'll use a format reward that will reward the model when the output includes `` and `` tags and additionally a length-based reward to discourage overthinking. Both functions have been extracted from [here](https://github.com/huggingface/open-r1/blob/main/src/open_r1/rewards.py)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jhgqx8kO99p_" }, "outputs": [], "source": [ "import re\n", "\n", "def format_reward(completions, **kwargs):\n", " \"\"\"Reward function that checks if the reasoning process is enclosed within and tags, while the final answer is enclosed within and tags.\"\"\"\n", " pattern = r\".*?.*?.*?\"\n", "\n", " matches = []\n", " for item in completions:\n", " if isinstance(item, list):\n", " text = item[0]['content']\n", " else:\n", " text = item\n", " match = re.match(pattern, text, re.DOTALL | re.MULTILINE)\n", " matches.append(match)\n", "\n", " return [1.0 if match else 0.0 for match in matches]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "sVmzQ_wL99p_" }, "outputs": [], "source": [ "from math_verify import LatexExtractionConfig, parse, verify\n", "from latex2sympy2_extended import NormalizationConfig\n", "\n", "\n", "def len_reward(completions, solution, **kwargs) -> float:\n", " \"\"\"Compute length-based rewards to discourage overthinking and promote token efficiency.\n", "\n", " Taken from the Kimi 1.5 tech report: https://huggingface.co/papers/2501.12599\n", "\n", " Args:\n", " completions: List of model completions\n", " solution: List of ground truth solutions\n", "\n", " Returns:\n", " List of rewards where:\n", " - For correct answers: reward = 0.5 - (len - min_len)/(max_len - min_len)\n", " - For incorrect answers: reward = min(0, 0.5 - (len - min_len)/(max_len - min_len))\n", " \"\"\"\n", " contents = []\n", " for item in completions:\n", " if isinstance(item, list):\n", " text = item[0]['content']\n", " else:\n", " text = item\n", " contents.append(text)\n", "\n", " # First check correctness of answers\n", " correctness = []\n", " for content, sol in zip(contents, solution):\n", " gold_parsed = parse(\n", " sol,\n", " extraction_mode=\"first_match\",\n", " extraction_config=[LatexExtractionConfig()],\n", " )\n", " if len(gold_parsed) == 0:\n", " # Skip unparsable examples\n", " correctness.append(True) # Treat as correct to avoid penalizing\n", " print(\"Failed to parse gold solution: \", sol)\n", " continue\n", "\n", " answer_parsed = parse(\n", " content,\n", " extraction_config=[\n", " LatexExtractionConfig(\n", " normalization_config=NormalizationConfig(\n", " nits=False,\n", " malformed_operators=False,\n", " basic_latex=True,\n", " equations=True,\n", " boxed=True,\n", " units=True,\n", " ),\n", " boxed_match_priority=0,\n", " try_extract_without_anchor=False,\n", " )\n", " ],\n", " extraction_mode=\"first_match\",\n", " )\n", " correctness.append(verify(answer_parsed, gold_parsed))\n", "\n", " # Calculate lengths\n", " lengths = [len(content) for content in contents]\n", " min_len = min(lengths)\n", " max_len = max(lengths)\n", "\n", " # If all responses have the same length, return zero rewards\n", " if max_len == min_len:\n", " return [0.0] * len(completions)\n", "\n", " rewards = []\n", " for length, is_correct in zip(lengths, correctness):\n", " lambda_val = 0.5 - (length - min_len) / (max_len - min_len)\n", "\n", " if is_correct:\n", " reward = lambda_val\n", " else:\n", " reward = min(0, lambda_val)\n", "\n", " rewards.append(float(reward))\n", "\n", " return rewards" ] }, { "cell_type": "markdown", "metadata": { "id": "9xBL7Rni9LZb" }, "source": [ "After defining the reward function(s), we can define the `GRPOConfig`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "pcv6KXUD99qA" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"Ministral-3-3B-Instruct-trl-grpo\"\n", "\n", "# Configure training arguments using GRPOConfig\n", "training_args = GRPOConfig(\n", " learning_rate=2e-5,\n", " #num_train_epochs=1,\n", " max_steps=100, # Number of dataset passes. For full trainings, use `num_train_epochs` instead\n", "\n", " # Parameters that control the data preprocessing\n", " per_device_train_batch_size=2,\n", " max_completion_length=1024, # default: 256 # Max completion length produced during training\n", " num_generations=2, # 2, # default: 8 # Number of generations produced during training for comparison\n", "\n", " fp16=False,\n", " bf16=False,\n", "\n", " # Parameters related to reporting and saving\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=1, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id = output_dir,\n", "\n", " # Hub integration\n", " push_to_hub=True,\n", " log_completions=True,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "O0q3myQg927v" }, "source": [ "Configure the GRPO Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-zd7s5Cs99qA" }, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "trainer = GRPOTrainer(\n", " model=model,\n", " reward_funcs=[format_reward, len_reward],\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "kQC7Q5kg95xq" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "iF7cnD0T99qA" }, "outputs": [], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "YazYtLAe97Dc" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Ynhxdv3a99qA" }, "outputs": [], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "SmcYN5yW99IP" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "mi-exH7699qA" }, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "saarW87Y9_-R" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "m3mlwQl699qA" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_id)" ] }, { "cell_type": "markdown", "metadata": { "id": "nfqvO0qw-OvS" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "B7usNBq699qA" }, "outputs": [], "source": [ "from transformers import Mistral3ForConditionalGeneration, MistralCommonBackend\n", "from peft import PeftModel\n", "\n", "base_model = model_name\n", "adapter_model = f\"{output_dir}\" # Replace with your HF username or organization\n", "\n", "model = Mistral3ForConditionalGeneration.from_pretrained(base_model, dtype=\"float32\", device_map=\"auto\")\n", "model = PeftModel.from_pretrained(model, adapter_model)\n", "\n", "tokenizer = MistralCommonBackend.from_pretrained(base_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "XnIOkXfy99qA" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0le5gBl_99qA" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "import base64\n", "from io import BytesIO\n", "\n", "dataset_id = 'lmms-lab/multimodal-open-r1-8k-verified'\n", "train_dataset = load_dataset(dataset_id, split='train[:5%]')\n", "\n", "problem = train_dataset[0]['problem']\n", "image = train_dataset[0]['image']\n", "\n", "buffer = BytesIO()\n", "image.save(buffer, format=\"JPEG\")\n", "image_bytes = buffer.getvalue()\n", "image_b64 = base64.b64encode(image_bytes).decode(\"utf-8\")\n", "\n", "messages = [\n", " {\n", " \"role\": \"system\", \"content\": [\n", " {\"type\": \"text\", \"text\": SYSTEM_PROMPT}\n", " ]\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\n", " \"type\": \"image_url\",\n", " \"image_url\": {\n", " \"url\": f\"data:image/jpeg;base64,{image_b64}\"\n", " },\n", " },\n", " {\"type\": \"text\", \"text\": problem},\n", " ],\n", " },\n", "]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "f9PgBCD499qA" }, "outputs": [], "source": [ "messages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ENOGILKk99qA" }, "outputs": [], "source": [ "import torch\n", "\n", "tokenized = tokenizer.apply_chat_template(messages, return_tensors=\"pt\", return_dict=True)\n", "tokenized[\"input_ids\"] = tokenized[\"input_ids\"].to(device=\"cuda\")\n", "tokenized[\"pixel_values\"] = tokenized[\"pixel_values\"].to(dtype=torch.bfloat16, device=\"cuda\")\n", "image_sizes = [tokenized[\"pixel_values\"].shape[-2:]]\n", "\n", "output = model.generate(\n", " **tokenized,\n", " image_sizes=image_sizes,\n", " max_new_tokens=512,\n", ")[0]\n", "\n", "decoded_output = tokenizer.decode(output[len(tokenized[\"input_ids\"][0]):])\n", "print(decoded_output)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/grpo_qwen3_vl.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "-J8iGzLf4rUJ" }, "source": [ "# GRPO Qwen3-VL with QLoRA using TRL\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_qwen3_vl.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "\n", "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can fine-tune cutting edge vision language models. It comes with support for quantized parameter efficient fine-tuning technique **QLoRA**, so we can use free Colab (T4 GPU) to fine-tune models like [Qwen3-VL](https://huggingface.co/collections/Qwen/qwen3-vl-68d2a7c1b8a8afce4ebd2dbe).\n", "\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)\n", "- [More Qwen3-VL Fine-tuning Examples (including TRL scripts)](https://github.com/QwenLM/Qwen3-VL/tree/main/qwen-vl-finetune/)" ] }, { "cell_type": "markdown", "metadata": { "id": "NvrzGRnu48Vz" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, and **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8CfZlUevmkg7" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" bitsandbytes trackio math_verify" ] }, { "cell_type": "markdown", "metadata": { "id": "gpzI6omi7728" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4Ncx0wYtnYCW" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "V_Zylc4t79-n" }, "source": [ "## Load dataset\n", "\n", "\n", "We'll load the [**lmms-lab/multimodal-open-r1-8k-verified**](https://huggingface.co/datasets/lmms-lab/multimodal-open-r1-8k-verified) dataset from the Hugging Face Hub using the `datasets` library.\n", "\n", "This dataset contains maths problems with the image representing the problem, along with the solution in thinking format specially tailored for VLMs. By training our model with this dataset, it'll improve its maths and thinking reasoning.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "TzXogU24F_QR" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_id = 'lmms-lab/multimodal-open-r1-8k-verified'\n", "train_dataset = load_dataset(dataset_id, split='train[:5%]')" ] }, { "cell_type": "markdown", "metadata": { "id": "gVV7RoRN8zk5" }, "source": [ "In addition to the `problem` and `image` columns, we also include a custom system prompt to tell the model how we'd like the generation.\n", "\n", "The system prompt is extracted from DeepSeek R1. Refer to [this previous recipe](https://huggingface.co/learn/cookbook/fine_tuning_llm_grpo_trl) for more details.\n", "\n", "We convert the dataset samples into conversation samples, including the system prompt and one image and problem description per sample, since this is how the GRPO trainer expects them.\n", "\n", "We also set `padding_side=\"left\"` to ensure that generated completions during training are concatenated directly after the prompt, which is essential for GRPO to correctly compare token-level probabilities between preferred and rejected responses." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ZT1JfiiTGExB" }, "outputs": [], "source": [ "from transformers import AutoProcessor\n", "\n", "model_name = \"Qwen/Qwen3-VL-4B-Instruct\" # \"Qwen/Qwen3-VL-8B-Instruct\"\n", "processor = AutoProcessor.from_pretrained(model_name, padding_side=\"left\")\n", "\n", "SYSTEM_PROMPT = (\n", " \"You are a helpful AI Assistant that provides well-reasoned and detailed responses. \"\n", " \"You first think about the reasoning process as an internal monologue and then provide the user with the answer. \"\n", " \"Respond in the following format: \\n...\\n\\n\\n...\\n\"\n", ")\n", "\n", "\n", "def make_conversation(example):\n", " prompt = [\n", " {\n", " \"role\": \"system\",\n", " \"content\": [{\"type\": \"text\", \"text\": SYSTEM_PROMPT}],\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\"type\": \"image\", \"image\": example[\"image\"]},\n", " {\"type\": \"text\", \"text\": example[\"problem\"]},\n", " ],\n", " },\n", " ]\n", " return {\"prompt\": prompt, \"image\": example[\"image\"]}\n", "\n", "train_dataset = train_dataset.map(make_conversation)" ] }, { "cell_type": "markdown", "metadata": { "id": "5txAuMAa8ock" }, "source": [ "Let's review one example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "PDXQd5Jk2Bqe" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "hzSR_56wxKDA" }, "outputs": [], "source": [ "train_dataset = train_dataset.remove_columns(['problem', 'original_question', 'original_answer'])" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "T9rCkeqDODba" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "markdown", "metadata": { "id": "YY3uMp909Eqy" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gt05dgXgm9QR" }, "outputs": [], "source": [ "from transformers import Qwen3VLForConditionalGeneration, BitsAndBytesConfig\n", "import torch\n", "\n", "model = Qwen3VLForConditionalGeneration.from_pretrained(\n", " model_name, dtype=\"float32\",\n", " device_map=\"auto\",\n", " quantization_config=BitsAndBytesConfig(\n", " load_in_4bit=True,\n", " bnb_4bit_use_double_quant=True,\n", " bnb_4bit_quant_type=\"nf4\",\n", " bnb_4bit_compute_dtype=torch.float16\n", " ),\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "WZGf-GF09Gsc" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter** — a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ME1im5gh2LFg" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different VLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=8,\n", " lora_alpha=32,\n", " lora_dropout=0.1,\n", " target_modules=[\"q_proj\", \"v_proj\"],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "mDq4V6dN9MGk" }, "source": [ "## Train model\n", "\n", "We'll configure **GRPO** using `GRPOConfig`, keeping the parameters minimal so the training fits on a free Colab instance. You can adjust these settings if more resources are available. For full details on all available parameters, check the [TRL GRPOConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.GRPOConfig).\n", "\n", "First, we need to define the rewards functions that the training algorithm will use to improve the model. In this case, we'll include two reward functions.\n", "We'll use a format reward that will reward the model when the output includes `` and `` tags and additionally a length-based reward to discourage overthinking. Both functions have been extracted from [here](https://github.com/huggingface/open-r1/blob/main/src/open_r1/rewards.py)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Dqp3TfUwHUxW" }, "outputs": [], "source": [ "import re\n", "\n", "def format_reward(completions, **kwargs):\n", " \"\"\"Reward function that checks if the reasoning process is enclosed within and tags, while the final answer is enclosed within and tags.\"\"\"\n", " pattern = r\"^\\n.*?\\n\\n\\n.*?\\n$\"\n", " matches = [re.match(pattern, content, re.DOTALL | re.MULTILINE) for content in completions]\n", " return [1.0 if match else 0.0 for match in matches]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "rxNPUp7RBFcz" }, "outputs": [], "source": [ "from math_verify import LatexExtractionConfig, parse, verify\n", "from latex2sympy2_extended import NormalizationConfig\n", "\n", "\n", "def len_reward(completions, solution, **kwargs) -> float:\n", " \"\"\"Compute length-based rewards to discourage overthinking and promote token efficiency.\n", "\n", " Taken from the Kimi 1.5 tech report: https://huggingface.co/papers/2501.12599\n", "\n", " Args:\n", " completions: List of model completions\n", " solution: List of ground truth solutions\n", "\n", " Returns:\n", " List of rewards where:\n", " - For correct answers: reward = 0.5 - (len - min_len)/(max_len - min_len)\n", " - For incorrect answers: reward = min(0, 0.5 - (len - min_len)/(max_len - min_len))\n", " \"\"\"\n", " contents = completions\n", "\n", " # First check correctness of answers\n", " correctness = []\n", " for content, sol in zip(contents, solution):\n", " gold_parsed = parse(\n", " sol,\n", " extraction_mode=\"first_match\",\n", " extraction_config=[LatexExtractionConfig()],\n", " )\n", " if len(gold_parsed) == 0:\n", " # Skip unparsable examples\n", " correctness.append(True) # Treat as correct to avoid penalizing\n", " print(\"Failed to parse gold solution: \", sol)\n", " continue\n", "\n", " answer_parsed = parse(\n", " content,\n", " extraction_config=[\n", " LatexExtractionConfig(\n", " normalization_config=NormalizationConfig(\n", " nits=False,\n", " malformed_operators=False,\n", " basic_latex=True,\n", " equations=True,\n", " boxed=True,\n", " units=True,\n", " ),\n", " boxed_match_priority=0,\n", " try_extract_without_anchor=False,\n", " )\n", " ],\n", " extraction_mode=\"first_match\",\n", " )\n", " correctness.append(verify(answer_parsed, gold_parsed))\n", "\n", " # Calculate lengths\n", " lengths = [len(content) for content in contents]\n", " min_len = min(lengths)\n", " max_len = max(lengths)\n", "\n", " # If all responses have the same length, return zero rewards\n", " if max_len == min_len:\n", " return [0.0] * len(completions)\n", "\n", " rewards = []\n", " for length, is_correct in zip(lengths, correctness):\n", " lambda_val = 0.5 - (length - min_len) / (max_len - min_len)\n", "\n", " if is_correct:\n", " reward = lambda_val\n", " else:\n", " reward = min(0, lambda_val)\n", "\n", " rewards.append(float(reward))\n", "\n", " return rewards\n" ] }, { "cell_type": "markdown", "metadata": { "id": "9xBL7Rni9LZb" }, "source": [ "After defining the reward function(s), we can define the `GRPOConfig`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "OEmRM0rIHXQ4" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"Qwen3-VL-4B-Instruct-trl-grpo\"\n", "\n", "# Configure training arguments using GRPOConfig\n", "training_args = GRPOConfig(\n", " learning_rate=2e-5,\n", " #num_train_epochs=1,\n", " max_steps=100, # Number of dataset passes. For full trainings, use `num_train_epochs` instead\n", "\n", " # Parameters that control the data preprocessing\n", " per_device_train_batch_size=2,\n", " max_completion_length=1024, # default: 256 # Max completion length produced during training\n", " num_generations=2, # 2, # default: 8 # Number of generations produced during training for comparison\n", "\n", " fp16=True,\n", "\n", " # Parameters related to reporting and saving\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=1, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", "\n", " # Hub integration\n", " push_to_hub=True,\n", " log_completions=True\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "O0q3myQg927v" }, "source": [ "Configure the GRPO Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "z5JxkmS9HqD5", "outputId": "2b39338e-2194-4829-fc54-5e286566fd28" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/peft/mapping_func.py:73: UserWarning: You are trying to modify a model with PEFT for a second time. If you want to reload the model with a different config, make sure to call `.unload()` before.\n", " warnings.warn(\n", "/usr/local/lib/python3.12/dist-packages/peft/tuners/tuners_utils.py:196: UserWarning: Already found a `peft_config` attribute in the model. This will lead to having multiple adapters in the model. Make sure to know what you are doing!\n", " warnings.warn(\n" ] } ], "source": [ "from trl import GRPOTrainer\n", "\n", "trainer = GRPOTrainer(\n", " model=model,\n", " reward_funcs=[format_reward, len_reward],\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "kQC7Q5kg95xq" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "naG_7qlYyBP6" }, "outputs": [], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "YazYtLAe97Dc" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "pbJXrhA0ywra" }, "outputs": [], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "SmcYN5yW99IP" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "TrrwP4ADMmrp" }, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "saarW87Y9_-R" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "71A8aqEyyETA" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_id)" ] }, { "cell_type": "markdown", "metadata": { "id": "nfqvO0qw-OvS" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "R8T2uFQVyFeH" }, "outputs": [], "source": [ "from transformers import Qwen3VLForConditionalGeneration, AutoProcessor\n", "from peft import PeftModel\n", "\n", "base_model = model_name\n", "adapter_model = f\"{output_dir}\" # Replace with your HF username or organization\n", "\n", "model = Qwen3VLForConditionalGeneration.from_pretrained(base_model, dtype=\"float32\", device_map=\"auto\")\n", "model = PeftModel.from_pretrained(model, adapter_model)\n", "\n", "processor = AutoProcessor.from_pretrained(base_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "dPBHP0CpLa6K" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "cG5-ccGRyHgo" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_id = 'lmms-lab/multimodal-open-r1-8k-verified'\n", "train_dataset = load_dataset(dataset_id, split='train[:5%]')\n", "\n", "problem = train_dataset[0]['problem']\n", "image = train_dataset[0]['image']\n", "\n", "messages = [\n", " {\n", " \"role\": \"system\", \"content\": [\n", " {\"type\": \"text\", \"text\": SYSTEM_PROMPT}\n", " ]\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\"type\": \"image\", \"image\": image},\n", " {\"type\": \"text\", \"text\": problem},\n", " ],\n", " },\n", "]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "r_70q_8lLgfV" }, "outputs": [], "source": [ "messages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "PX92MjqlyIwB" }, "outputs": [], "source": [ "inputs = processor.apply_chat_template(\n", " messages,\n", " add_generation_prompt=True,\n", " tokenize=True,\n", " return_tensors=\"pt\",\n", " return_dict=True,\n", ").to(model.device)\n", "\n", "# Inference: Generation of the output\n", "generated_ids = model.generate(**inputs, max_new_tokens=500)\n", "generated_ids_trimmed = [\n", " out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)\n", "]\n", "output_text = processor.batch_decode(\n", " generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False\n", ")\n", "print(output_text)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/grpo_rnj_1_instruct.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "-J8iGzLf4rUJ" }, "source": [ "# GRPO EssentialAI/rnj-1-instruct with QLoRA using TRL\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_rnj_1_instruct.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "\n", "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can fine-tune cutting edge large language models. It comes with support for quantized parameter efficient fine-tuning technique **QLoRA**, so we can use Colab to fine-tune models like [EssentialAI/rnj-1-instruct](https://huggingface.co/collections/EssentialAI/rnj-1).\n", "\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)\n", "\n", "In this notebook, we'll add reasoning capabilities to the model, teaching it to generate reasoning traces (``) before giving us the final answer (``)." ] }, { "cell_type": "markdown", "metadata": { "id": "NvrzGRnu48Vz" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, and **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8VOdRz9fgFa8" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" bitsandbytes trackio math_verify" ] }, { "cell_type": "markdown", "metadata": { "id": "gpzI6omi7728" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "d3j3BsdQgFa8" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "V_Zylc4t79-n" }, "source": [ "## Load dataset\n", "\n", "\n", "We'll load the [**AI-MO/NuminaMath-TIR**](https://huggingface.co/datasets/AI-MO/NuminaMath-TIR) dataset from the Hugging Face Hub using the `datasets` library.\n", "\n", "This dataset contains maths problems, along with the solution in thinking format specially tailored for LLMs. By training our model with this dataset, it'll improve its maths and thinking reasoning.\n", "\n", "> We only use a subset for educational purposes. In a real scenario, we'd use the complete dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "YSuLNZAmgFa9" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_id = 'AI-MO/NuminaMath-TIR'\n", "train_dataset = load_dataset(dataset_id, split='train[:5%]')" ] }, { "cell_type": "markdown", "metadata": { "id": "gVV7RoRN8zk5" }, "source": [ "In addition to the current columns, we also include a custom system prompt to tell the model how we'd like the generation.\n", "\n", "This system prompt is an adapted version of the original one extracted from **DeepSeek R1**. For additional background, see [this previous recipe](https://huggingface.co/learn/cookbook/fine_tuning_llm_grpo_trl). We extend the prompt with **examples** and a **more explicit, verbose formulation** to make the desired behavior easier for the model to learn. Depending on your goals, you may further enrich the prompt to simplify learning, or intentionally shorten and harden it to encourage more robust and generalizable behavior.\n", "\n", "We convert the dataset samples into conversation samples, including the system prompt and problem description per sample, since this is how the GRPO trainer expects them.\n", "\n", "We also set `padding_side=\"left\"` to ensure that generated completions during training are concatenated directly after the prompt, which is essential for GRPO to correctly compare token-level probabilities between preferred and rejected responses." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "vr9t-9Z5gFa9" }, "outputs": [], "source": [ "SYSTEM_PROMPT = \"\"\"A conversation between User and Assistant. The user asks a question, and the Assistant solves it.\n", "The assistant first thinks about the reasoning process in the mind and then provides the user with the answer.\n", "The reasoning process and answer are enclosed within and tags.\n", "Use exactly one ... block followed by exactly one ... block.\n", "\n", "Examples:\n", "\n", "User: What is 2 + 2?\n", "Assistant:\n", "\n", "I will add 2 and 2 together.\n", "\n", "4\n", "\n", "User: What is 3 × 5?\n", "Assistant:\n", "\n", "I will multiply 3 by 5.\n", "\n", "15\n", "\n", "User: Find the GCD of 12 and 18.\n", "Assistant:\n", "\n", "I will list the divisors of 12 and 18 and find the greatest one they have in common.\n", "\n", "6\n", "\"\"\"\n", "\n", "def make_conversation(example):\n", " return {\n", " \"prompt\": [\n", " {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n", " {\"role\": \"user\", \"content\": example[\"problem\"]},\n", " ],\n", " }\n", "\n", "train_dataset = train_dataset.map(make_conversation)" ] }, { "cell_type": "markdown", "metadata": { "id": "5txAuMAa8ock" }, "source": [ "Let's review one example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jZtkB0D9gFa9" }, "outputs": [], "source": [ "print(train_dataset[0])" ] }, { "cell_type": "markdown", "metadata": { "id": "FtdKjmyFZImL" }, "source": [ "And remove the columns that are not needed for training:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Ai4F1GaPgFa-" }, "outputs": [], "source": [ "train_dataset = train_dataset.remove_columns(['messages', 'problem'])\n", "print(train_dataset)" ] }, { "cell_type": "markdown", "metadata": { "id": "YY3uMp909Eqy" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "DSKcUQ9RgFa-" }, "outputs": [], "source": [ "from transformers import AutoModelForCausalLM, BitsAndBytesConfig\n", "import torch\n", "\n", "model_name = \"EssentialAI/rnj-1-instruct\"\n", "\n", "model = AutoModelForCausalLM.from_pretrained(\n", " model_name,\n", " dtype=\"float32\",\n", " device_map=\"auto\",\n", " quantization_config=BitsAndBytesConfig(\n", " load_in_4bit=True,\n", " bnb_4bit_use_double_quant=True,\n", " bnb_4bit_quant_type=\"nf4\",\n", " bnb_4bit_compute_dtype=torch.float16\n", " ),\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "WZGf-GF09Gsc" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter**, a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "nMMlDxJSgFa-" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different LLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=32,\n", " lora_alpha=32,\n", " target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\",],\n", ")\n" ] }, { "cell_type": "markdown", "metadata": { "id": "mDq4V6dN9MGk" }, "source": [ "## Train model\n", "\n", "We'll configure **GRPO** using `GRPOConfig`, keeping the parameters minimal so the training fits on a Colab instance. You can adjust these settings depending on the resources available. For full details on all available parameters, check the [TRL GRPOConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.GRPOConfig).\n", "\n", "First, we need to define the rewards functions that the training algorithm will use to improve the model. In this case, we'll include just one reward function.\n", "We'll use a format reward that will reward the model when the output includes `` and `` tags. This is a simplification of the pipeline for educational purposes, but in a real scenario, you'd at least all need a reward function to check the correctness of the model answer. The function has been extracted from [here](https://github.com/huggingface/open-r1/blob/main/src/open_r1/rewards.py).\n", "\n", "> 💡 **Note**: \n", "> You can further refine this reward by making it more granular. For example, assigning partial rewards when `` and `` appear independently, or when they are present but incorrectly ordered. This can make the learning signal denser and speed up early training. However, overly simplifying the reward may reduce robustness, even if it helps the model converge faster. In practice, there is a trade-off between ease of learning and the generalization quality of the final model." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Rtx5owCRgFa-" }, "outputs": [], "source": [ "import re\n", "\n", "def format_reward(completions, **kwargs):\n", " \"\"\"Reward function that checks if the reasoning process is enclosed within and tags, while the final answer is enclosed within and tags.\"\"\"\n", " pattern = r\".*?.*?.*?\"\n", "\n", " matches = []\n", " for item in completions:\n", " if isinstance(item, list):\n", " text = item[0]['content']\n", " else:\n", " text = item\n", " match = re.match(pattern, text, re.DOTALL | re.MULTILINE)\n", " matches.append(match)\n", "\n", " return [1.0 if match else 0.0 for match in matches]" ] }, { "cell_type": "markdown", "metadata": { "id": "9xBL7Rni9LZb" }, "source": [ "After defining the reward function(s), we can define the `GRPOConfig`. You can adapt the values in the config depending on your training setting and even fit the training in more constrained setups like free Colab (T4)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "rJ0VfG3wgFa-" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"EssentialAI-rnj-1-instruct-trl-grpo\"\n", "\n", "# Configure training arguments using GRPOConfig\n", "training_args = GRPOConfig(\n", " learning_rate=2e-5, # Learning rate used during traing\n", " num_train_epochs=1, # Number of full dataset passes. For testing, use `max_steps` instead\n", " #max_steps=100,\n", "\n", " # Parameters that control the data preprocessing\n", " per_device_train_batch_size=8,\n", " max_completion_length=256, # default: 256 # Max completion length produced during training\n", " num_generations=8, # default: 8 # Number of generations produced during training for comparison\n", "\n", " # Parameters related to reporting and saving\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=10, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id = output_dir, # HF Space where you trackio will be\n", "\n", " # Hub integration\n", " push_to_hub=True, # Push the resulted model to the Hub\n", " log_completions=True, # Log completions during training\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "O0q3myQg927v" }, "source": [ "Configure the GRPO Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "aW7Gi4nXgFa-" }, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "trainer = GRPOTrainer(\n", " model=model,\n", " reward_funcs=[format_reward],\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "kQC7Q5kg95xq" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "OJdVlC_mgFa_" }, "outputs": [], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "YazYtLAe97Dc" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Mtv8s7rBgFa_" }, "outputs": [], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "SmcYN5yW99IP" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-ROfX8e9gFa_" }, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "saarW87Y9_-R" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "09zYXJ3GgFa_" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_id)" ] }, { "cell_type": "markdown", "metadata": { "id": "nfqvO0qw-OvS" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "9Yk9RAABgFa_" }, "outputs": [], "source": [ "output_dir = 'sergiopaniego/EssentialAI-rnj-1-instruct-trl-grpo'\n", "model_name = \"EssentialAI/rnj-1-instruct\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "CdzlQcCAgFa_" }, "outputs": [], "source": [ "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "from peft import PeftModel\n", "\n", "base_model = model_name\n", "adapter_model = f\"{output_dir}\" # Replace with your HF username or organization\n", "\n", "model = AutoModelForCausalLM.from_pretrained(base_model, dtype=\"float32\", device_map=\"auto\")\n", "model = PeftModel.from_pretrained(model, adapter_model)\n", "\n", "tokenizer = AutoTokenizer.from_pretrained(base_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LZgjlAu-gFa_" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gjY6TqQHgFa_" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_id = 'AI-MO/NuminaMath-TIR'\n", "train_dataset = load_dataset(dataset_id, split='train[:5%]')\n", "\n", "problem = train_dataset[0]['problem']\n", "\n", "messages = [\n", " {\n", " \"role\": \"system\", \"content\": [\n", " {\"type\": \"text\", \"text\": SYSTEM_PROMPT}\n", " ]\n", " },\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\"type\": \"text\", \"text\": problem},\n", " ],\n", " },\n", "]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "eaVubGYmgFa_" }, "outputs": [], "source": [ "messages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "2M6Xh4JMgFa_" }, "outputs": [], "source": [ "input_ids = tokenizer.apply_chat_template(\n", " messages,\n", " add_generation_prompt=True,\n", " return_tensors=\"pt\",\n", " return_dict=False,\n", ").to(model.device)\n", "\n", "# --- Generate Prediction --- #\n", "print(\"Generating prediction...\")\n", "output_ids = model.generate(\n", " input_ids,\n", " max_new_tokens=50,\n", " pad_token_id=tokenizer.eos_token_id,\n", " do_sample=True,\n", " temperature=0.2,\n", " top_p=0.95\n", ")\n", "\n", "response = tokenizer.decode(output_ids[0][input_ids.shape[-1]:], skip_special_tokens=True)\n", "print(response)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "A100", "provenance": [] }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/grpo_trl_lora_qlora.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "27ozP4Uy-Cz2" }, "source": [ "# Group Relative Policy Optimization (GRPO) with LoRA/QLoRA using TRL — on a Free Colab Notebook\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/grpo_trl_lora_qlora.ipynb)" ] }, { "cell_type": "markdown", "metadata": { "id": "eOjY4AR1-QnF" }, "source": [ "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "Easily fine-tune **Large Language Models (LLMs)** or **Vision-Language Models (VLMs)** with **LoRA** or **QLoRA** using the [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl) library by Hugging Face and Group Relative Policy Optimization (GRPO) — all within a **free Google Colab notebook** powered by a **T4 GPU**.\n", "\n", "Thanks to the **built-in memory and training optimizations in TRL**, including LoRA, quantization, gradient checkpointing, and optimized attention kernels, it is possible to **fine-tune a 7B model on a free T4** with a **~7× reduction in memory consumption** compared to naive FP16 training.\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)" ] }, { "cell_type": "markdown", "metadata": { "id": "w2TnJ6ta-2zj" }, "source": [ "## Key concepts\n", "\n", "- **GRPO**: A reinforcement learning algorithm that optimizes a policy by comparing multiple generated responses for the same prompt and updating the model based on their relative rewards, without requiring a separate value model.\n", "- **LoRA**: Updates only a few low-rank parameters, reducing training cost and memory.\n", "- **QLoRA**: A quantized version of LoRA that enables even larger models to fit on small GPUs.\n", "- **TRL**: The Hugging Face library that makes fine-tuning and reinforcement learning simple and efficient.\n", "\n", "Learn how to perform **GRPO (Group Relative Policy Optimization)** with **LoRA/QLoRA** using **TRL**." ] }, { "cell_type": "markdown", "metadata": { "id": "EzScUBxoT4Nt" }, "source": [ "This table demonstrates how **progressively enabling efficiency techniques** affects **memory usage** and **training throughput** across different hardware configurations. \n", "The techniques range from naive FP16 training to **LoRA, quantization, Liger kernels, paged_adamw_8bit, and gradient checkpointing**.\n", "\n", "| Configuration | LoRA | Quant | Liger | Optimizer | Grad. Ckpt | attn_impl | VRAM (T4) GB | VRAM (A100-40GB)| VRAM (A100-80GB) | Tokens/s (T4) | Tokens/s (A100-40GB) | Tokens/s (A100-80GB) | Status (T4) |\n", "|--------------|------|-------|-------|-----------|------------|-----------|---------------|----------------|---------|---------|---------------|------------------|-------------|\n", "| **Worst (naive FP16)** | ❌ | ❌ | ❌ | AdamW | ❌ | eager | OOM | OOM | 62 GB | - | - | 0.06 it/s | ❌ |\n", "| **Best (all optimizations)** | ✅ | ✅ | ✅ | paged_adamw_8bit | ✅ | sdpa | 9.2 GB | 9.6 GB | 9.6 GB | 0.01 it/s | 0.03 it/s | 0.04 it/s | ✅ |\n", "\n", "With all efficiency techniques enabled, **memory usage on Colab T4 is reduced by ~7×**, making it possible to **fine-tune a 7B model on free Colab** where naive FP16 training would fail.\n", "\n", "> A small trade-off in training speed is observed, but the **VRAM reduction is the key enabler**. For faster training on compatible hardware, **vLLM** can also be leveraged.\n", "\n", "> 💡 Note: For a fair comparison, the number of generations and the batch size were not changed." ] }, { "cell_type": "markdown", "metadata": { "id": "9RFq6Op7rjc3" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training, and **liger-kernel** for more efficient training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "c2jy45nfWbdo" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" bitsandbytes trackio math_verify liger-kernel" ] }, { "cell_type": "markdown", "metadata": { "id": "B33zJG_Q_qb3" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "eec717d21e734c4da066763b4a6add7e" ] }, "id": "8zqnTyUDWbdo", "outputId": "62d71aaf-352b-4736-acb9-189d78654718" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "cTEw4xlFrhnQ" }, "source": [ "## Load Dataset\n", "\n", "In this step, we load the [**AI-MO/NuminaMath-TIR**](https://huggingface.co/datasets/AI-MO/NuminaMath-TIR) dataset from the Hugging Face Hub using the `datasets` library.\n", "This dataset focuses on **mathematical reasoning**, featuring problems that require step-by-step logical solutions.\n", "By fine-tuning a model that does not yet exhibit strong reasoning capabilities, it can learn to **generate structured reasoning steps**, enhancing both the model's **accuracy** and **interpretability** on math-related tasks.\n", "\n", "For efficiency, we'll load only a **small portion of the training split**:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zU5icx67Wbdp", "outputId": "6480b287-dc0e-4e79-feda-f5e4f41d2a82" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = 'AI-MO/NuminaMath-TIR'\n", "train_dataset = load_dataset(dataset_name, split='train[:5%]')" ] }, { "cell_type": "markdown", "metadata": { "id": "P1AIokQrBEGw" }, "source": [ "Let's check the structure of the dataset" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ff6Gx1TWWbdp", "outputId": "30d49bed-273a-47d9-d131-a677ca5a8b65" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dataset({\n", " features: ['problem', 'solution', 'messages'],\n", " num_rows: 3622\n", "})\n" ] } ], "source": [ "print(train_dataset)" ] }, { "cell_type": "markdown", "metadata": { "id": "QY5hkOqDBGns" }, "source": [ "Let's check one sample:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-y9c7i29Wbdp", "outputId": "760662ea-4db4-4b8e-c234-92ae2c8ecc17" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "{'problem': 'What is the coefficient of $x^2y^6$ in the expansion of $\\\\left(\\\\frac{3}{5}x-\\\\frac{y}{2}\\\\right)^8$? Express your answer as a common fraction.', 'solution': \"To determine the coefficient of \\\\(x^2y^6\\\\) in the expansion of \\\\(\\\\left(\\\\frac{3}{5}x - \\\\frac{y}{2}\\\\right)^8\\\\), we can use the binomial theorem.\\n\\nThe binomial theorem states:\\n\\\\[\\n(a + b)^n = \\\\sum_{k=0}^{n} \\\\binom{n}{k} a^{n-k} b^k\\n\\\\]\\n\\nIn this case, \\\\(a = \\\\frac{3}{5}x\\\\), \\\\(b = -\\\\frac{y}{2}\\\\), and \\\\(n = 8\\\\).\\n\\nWe are interested in the term that contains \\\\(x^2y^6\\\\). In the general term of the binomial expansion:\\n\\\\[\\n\\\\binom{8}{k} \\\\left(\\\\frac{3}{5}x\\\\right)^{8-k} \\\\left(-\\\\frac{y}{2}\\\\right)^k\\n\\\\]\\n\\nTo get \\\\(x^2\\\\), we need \\\\(8 - k = 2\\\\), thus \\\\(k = 6\\\\).\\n\\nSubstituting \\\\(k = 6\\\\) into the expression:\\n\\\\[\\n\\\\binom{8}{6} \\\\left(\\\\frac{3}{5}x\\\\right)^{8-6} \\\\left(-\\\\frac{y}{2}\\\\right)^6 = \\\\binom{8}{6} \\\\left(\\\\frac{3}{5}x\\\\right)^2 \\\\left(-\\\\frac{y}{2}\\\\right)^6\\n\\\\]\\n\\nNow, we will compute each part of this expression.\\n\\n1. Calculate the binomial coefficient \\\\(\\\\binom{8}{6}\\\\).\\n2. Compute \\\\(\\\\left(\\\\frac{3}{5}\\\\right)^2\\\\).\\n3. Compute \\\\(\\\\left(-\\\\frac{y}{2}\\\\right)^6\\\\).\\n4. Combine everything together to get the coefficient of \\\\(x^2y^6\\\\).\\n\\nLet's compute these in Python.\\n```python\\nfrom math import comb\\n\\n# Given values\\nn = 8\\nk = 6\\n\\n# Calculate the binomial coefficient\\nbinom_coeff = comb(n, k)\\n\\n# Compute (3/5)^2\\na_term = (3/5)**2\\n\\n# Compute (-1/2)^6\\nb_term = (-1/2)**6\\n\\n# Combine terms to get the coefficient of x^2y^6\\ncoefficient = binom_coeff * a_term * b_term\\nprint(coefficient)\\n```\\n```output\\n0.1575\\n```\\nThe coefficient of \\\\(x^2y^6\\\\) in the expansion of \\\\(\\\\left(\\\\frac{3}{5}x - \\\\frac{y}{2}\\\\right)^8\\\\) is \\\\(0.1575\\\\). To express this as a common fraction, we recognize that:\\n\\n\\\\[ 0.1575 = \\\\frac{1575}{10000} = \\\\frac{63}{400} \\\\]\\n\\nThus, the coefficient can be expressed as:\\n\\n\\\\[\\n\\\\boxed{\\\\frac{63}{400}}\\n\\\\]\", 'messages': [{'content': 'What is the coefficient of $x^2y^6$ in the expansion of $\\\\left(\\\\frac{3}{5}x-\\\\frac{y}{2}\\\\right)^8$? Express your answer as a common fraction.', 'role': 'user'}, {'content': \"To determine the coefficient of \\\\(x^2y^6\\\\) in the expansion of \\\\(\\\\left(\\\\frac{3}{5}x - \\\\frac{y}{2}\\\\right)^8\\\\), we can use the binomial theorem.\\n\\nThe binomial theorem states:\\n\\\\[\\n(a + b)^n = \\\\sum_{k=0}^{n} \\\\binom{n}{k} a^{n-k} b^k\\n\\\\]\\n\\nIn this case, \\\\(a = \\\\frac{3}{5}x\\\\), \\\\(b = -\\\\frac{y}{2}\\\\), and \\\\(n = 8\\\\).\\n\\nWe are interested in the term that contains \\\\(x^2y^6\\\\). In the general term of the binomial expansion:\\n\\\\[\\n\\\\binom{8}{k} \\\\left(\\\\frac{3}{5}x\\\\right)^{8-k} \\\\left(-\\\\frac{y}{2}\\\\right)^k\\n\\\\]\\n\\nTo get \\\\(x^2\\\\), we need \\\\(8 - k = 2\\\\), thus \\\\(k = 6\\\\).\\n\\nSubstituting \\\\(k = 6\\\\) into the expression:\\n\\\\[\\n\\\\binom{8}{6} \\\\left(\\\\frac{3}{5}x\\\\right)^{8-6} \\\\left(-\\\\frac{y}{2}\\\\right)^6 = \\\\binom{8}{6} \\\\left(\\\\frac{3}{5}x\\\\right)^2 \\\\left(-\\\\frac{y}{2}\\\\right)^6\\n\\\\]\\n\\nNow, we will compute each part of this expression.\\n\\n1. Calculate the binomial coefficient \\\\(\\\\binom{8}{6}\\\\).\\n2. Compute \\\\(\\\\left(\\\\frac{3}{5}\\\\right)^2\\\\).\\n3. Compute \\\\(\\\\left(-\\\\frac{y}{2}\\\\right)^6\\\\).\\n4. Combine everything together to get the coefficient of \\\\(x^2y^6\\\\).\\n\\nLet's compute these in Python.\\n```python\\nfrom math import comb\\n\\n# Given values\\nn = 8\\nk = 6\\n\\n# Calculate the binomial coefficient\\nbinom_coeff = comb(n, k)\\n\\n# Compute (3/5)^2\\na_term = (3/5)**2\\n\\n# Compute (-1/2)^6\\nb_term = (-1/2)**6\\n\\n# Combine terms to get the coefficient of x^2y^6\\ncoefficient = binom_coeff * a_term * b_term\\nprint(coefficient)\\n```\\n```output\\n0.1575\\n```\\nThe coefficient of \\\\(x^2y^6\\\\) in the expansion of \\\\(\\\\left(\\\\frac{3}{5}x - \\\\frac{y}{2}\\\\right)^8\\\\) is \\\\(0.1575\\\\). To express this as a common fraction, we recognize that:\\n\\n\\\\[ 0.1575 = \\\\frac{1575}{10000} = \\\\frac{63}{400} \\\\]\\n\\nThus, the coefficient can be expressed as:\\n\\n\\\\[\\n\\\\boxed{\\\\frac{63}{400}}\\n\\\\]\", 'role': 'assistant'}]}\n" ] } ], "source": [ "print(train_dataset[0])" ] }, { "cell_type": "markdown", "metadata": { "id": "DiqBlxK_A0SD" }, "source": [ "We will adapt our dataset to a conversational format using a custom system prompt, guiding the LLM to generate both step-by-step reasoning and the final answer." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "RWxK5xFKWbdp" }, "outputs": [], "source": [ "SYSTEM_PROMPT = (\n", " \"A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant \"\n", " \"first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning \"\n", " \"process is enclosed strictly within and tags. \"\n", " \"After closing , the assistant MUST provide the final answer in plain text.\"\n", ")\n", "\n", "\n", "def make_conversation(example):\n", " return {\n", " \"prompt\": [\n", " {\"role\": \"system\", \"content\": SYSTEM_PROMPT},\n", " {\"role\": \"user\", \"content\": example[\"problem\"]},\n", " ],\n", " }\n", "\n", "train_dataset = train_dataset.map(make_conversation)" ] }, { "cell_type": "markdown", "metadata": { "id": "sND566XAC0kD" }, "source": [ "Let's take a look at an example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Q-kHUmpMWbdp", "outputId": "452beb3a-1091-46d4-997e-04b91562d66c" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'content': 'A conversation between User and Assistant. The user asks a question, and the Assistant solves it. The assistant first thinks about the reasoning process in the mind and then provides the user with the answer. The reasoning process is enclosed strictly within and tags. After closing , the assistant MUST provide the final answer in plain text.', 'role': 'system'}, {'content': 'What is the coefficient of $x^2y^6$ in the expansion of $\\\\left(\\\\frac{3}{5}x-\\\\frac{y}{2}\\\\right)^8$? Express your answer as a common fraction.', 'role': 'user'}]\n" ] } ], "source": [ "print(train_dataset[0]['prompt'])" ] }, { "cell_type": "markdown", "metadata": { "id": "bw0qcp-CC3G0" }, "source": [ "We'll remove the `messages` and `problem` columns, as we only need the custom `prompt` column and `solution` to verify the generated answer." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "SzbF3hdRWbdp", "outputId": "bd59a383-1d4e-4020-c232-79ce66073fd1" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Dataset({\n", " features: ['solution', 'prompt'],\n", " num_rows: 3622\n", "})\n" ] } ], "source": [ "train_dataset = train_dataset.remove_columns(['messages', 'problem'])\n", "print(train_dataset)" ] }, { "cell_type": "markdown", "metadata": { "id": "tvs5rjQBr7af" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "Below, choose your **preferred model**. All of the options have been tested on **free Colab instances**.\n", "\n", "> 💡 Note: Some models, such as Qwen2.5 and Qwen3, are known to have been pretrained on data that improves their math performance. Be cautious when selecting the appropriate model for training to ensure meaningful fine-tuning results ([source](https://thinkingmachines.ai/blog/lora/))." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "7_uaW3JfWbdp" }, "outputs": [], "source": [ "# Select one model below by uncommenting the line you want to use 👇\n", "## Qwen\n", "model_id, output_dir = \"Qwen/Qwen2-7B-Instruct\", \"t4-Qwen2-7B-Instruct-GRPO\" # ✅ ~9.2GB VRAM\n", "# model_id, output_dir = \"unsloth/qwen3-14b-unsloth-bnb-4bit\", \"qwen3-14b-unsloth-bnb-4bit-GRPO\" # ⚠️ OOM with this config; fits if GRPO params are reduced\n", "# model_id, output_dir = \"Qwen/Qwen3-8B\", \"Qwen3-8B-GRPO\" # ✅ ~9.9GB VRAM\n", "# model_id, output_dir = \"Qwen/Qwen2.5-7B-Instruct\", \"Qwen2.5-7B-Instruct-GRPO\" # ✅ ~9.2GB VRAM\n", "\n", "## Llama\n", "# model_id, output_dir = \"meta-llama/Llama-3.2-3B-Instruct\", \"Llama-3.2-3B-Instruct-GRPO\" # ✅ ~5.7GB VRAM\n", "# model_id, output_dir = \"meta-llama/Llama-3.1-8B-Instruct\", \"Llama-3.1-8B-Instruct-GRPO\" # ✅ ~9.5GB VRAM\n", "\n", "## LFM2.5\n", "# model_id, output_dir = \"LiquidAI/LFM2.5-1.2B-Instruct\", \"LFM2.5-1.2B-Instruct-GRPO\" # ✅ ~1.12 GB VRAM" ] }, { "cell_type": "markdown", "metadata": { "id": "aw__94OWDnER" }, "source": [ "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration (training without quantization consumes more memory).\n", "\n", "Let's load the selected model using `transformers`, configuring QLoRA via `bitsandbytes` (you can remove it if doing LoRA). We don't need to configure the tokenizer since the trainer takes care of that automatically." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "1130e5a744864ca5b5873731e4764983" ] }, "id": "o86TnTchWbdp", "outputId": "77a7e6c8-0360-40f1-eea7-b941be031366" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "1130e5a744864ca5b5873731e4764983", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading checkpoint shards: 0%| | 0/4 [00:00` format:\n", "\n", "```python\n", "def think_format_reward(completions: list[list[dict[str, str]]], **kwargs) -> list[float]:\n", " pattern = r\"^(?!.*)(.*?).*$\"\n", " completion_contents = [completion[0][\"content\"] for completion in completions]\n", " matches = [re.match(pattern, content, re.DOTALL | re.MULTILINE) for content in completion_contents]\n", " return [1.0 if match else 0.0 for match in matches]\n", "```\n", "\n", "In this notebook, we will use both `think_format_reward`, which rewards completions that correctly follow the `` format, and `reasoning_accuracy_reward`, which evaluates the correctness of the model's solution to the mathematical problem. Together, these rewards guide the model to generate **structured reasoning** while producing **accurate answers**." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "lj42Qs5vWbdp" }, "outputs": [], "source": [ "from trl.rewards import think_format_reward, reasoning_accuracy_reward" ] }, { "cell_type": "markdown", "metadata": { "id": "bFgYgxMbtbEZ" }, "source": [ "We'll configure **GRPO** using `GRPOConfig`, keeping the parameters minimal so that the training can run on a free Colab instance. You can adjust these settings if you have access to more resources. For a complete list of available parameters and their descriptions, refer to the [TRL GRPOConfig documentation](https://huggingface.co/docs/trl/grpo_trainer#trl.GRPOConfig).\n", "\n", "> 💡 Note: TRL supports using **vLLM** for generation during GRPO training, which can significantly speed up training. However, it increases VRAM usage since a separate vLLM process is active to handle generation. In this notebook, we do not enable vLLM because we are using **QLoRA**, which updates the quantized vLLM model weights at every step. Enabling vLLM in this setup can cause weight precision issues and make convergence more challenging. The configuration includes the vLLM parameters in case you want to experiment with it. Learn more about vLLM integration in TRL [here](https://huggingface.co/docs/trl/main/en/vllm_integration)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "JY11EQMhWbdp" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "# Configure training arguments using GRPOConfig\n", "training_args = GRPOConfig(\n", " # Training schedule / optimization\n", " learning_rate=2e-5, # Learning rate for the optimizer\n", " #num_train_epochs=1,\n", " max_steps=500, # Number of dataset passes. For full trainings, use `num_train_epochs` instead\n", "\n", " # Parameters that control GRPO training (you can adapt them)\n", " per_device_train_batch_size = 8,\n", " max_completion_length=256, # default: 256 # Max completion length produced during training\n", " num_generations=8, # default: 8 # Number of generations produced during trainig for comparison\n", "\n", " # Optimizations\n", " optim = \"paged_adamw_8bit\", # Optimizer\n", " use_liger_kernel=True, # Enable Liger kernel optimizations for faster training\n", "\n", " # Parameters related to reporting and saving\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=10, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id=output_dir, # HF Space where the experiment tracking will be saved\n", " log_completions=False, # Return model completions during training\n", "\n", " # Hub integration\n", " push_to_hub=True, # Automatically push the trained model to the Hugging Face Hub\n", " # The model will be saved under your Hub account in the repository named `output_dir`\n", " # vLLM params\n", " #use_vllm=False, # Activate vLLM training for faster training\n", " #vllm_mode='colocate',\n", " #vllm_gpu_memory_utilization=0.1,\n", " #vllm_enable_sleep_mode=True\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "-9LlOAvWFSor" }, "source": [ "Configure the `GRPOTrainer` by passing the previously defined `training_args`. To keep memory usage low, we are not using an evaluation dataset, but you can include one if desired. We also provide the reward functions that were imported earlier to guide the training process." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "iI_E9KCUWbdq" }, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "trainer = GRPOTrainer(\n", " model=model,\n", " reward_funcs=[think_format_reward, reasoning_accuracy_reward],\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "8dY7bK8FGLhh" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "PEVRGlrAWbdq", "outputId": "78fac9e4-4ae6-4836-bd10-c30b39059782" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GPU = Tesla T4. Max memory = 14.741 GB.\n", "6.773 GB of memory reserved.\n" ] } ], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "z-5xPtfIGQL5" }, "source": [ "And train!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Training on a T4 in Colab with the configuration defined in this notebook takes around 13 hours. If you're just experimenting, you can try the following quicker task ([source](https://huggingface.co/learn/llm-course/en/chapter12/5)):\n", "\n", "```python\n", "dataset = load_dataset(\"mlabonne/smoltldr\")\n", "\n", "# Reward function\n", "ideal_length = 50\n", "\n", "def reward_len(completions, **kwargs):\n", " return [-abs(ideal_length - len(completion)) for completion in completions]\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zl7-PmoXWbdq", "outputId": "f39c8c3c-43c2-4f2d-c98d-4c595ae1129f" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None, 'pad_token_id': 151643}.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "* Trackio project initialized: huggingface\n", "* Trackio metrics will be synced to Hugging Face Dataset: sergiopaniego/t4-Qwen2-7B-Instruct-GRPO-dataset\n", "* Creating new space: https://huggingface.co/spaces/sergiopaniego/t4-Qwen2-7B-Instruct-GRPO\n", "* View dashboard by going to: https://sergiopaniego-t4-Qwen2-7B-Instruct-GRPO.hf.space/\n" ] }, { "data": { "text/html": [ "

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Created new run: sergiopaniego-1766143600\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [500/500 13:05:04, Epoch 0/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
100.027900
20-0.011600
300.021500
400.033400
500.039400
600.010300
700.048200
800.067300
900.030600
1000.064000
1100.021500
1200.021400
1300.000000
140-0.028500
150-0.003100
1600.017300
170-0.024700
1800.003300
1900.000000
200-0.001400
2100.008000
2200.034300
2300.044600
2400.016400
250-0.015200
2600.016800
2700.042900
2800.031300
2900.006200
3000.043300
3100.029700
3200.001100
3300.027000
340-0.006700
3500.027200
3600.008200
370-0.015800
3800.007200
3900.012100
4000.000000
4100.010500
4200.019800
4300.000800
4400.003400
450-0.007900
460-0.011800
470-0.016300
480-0.002300
490-0.005500
5000.038000

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "iqAN-XLCGTGW" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4BeEwp5EWbds", "outputId": "668b8a2c-2eef-4e34-8d4a-2a43ccbbdc00" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "47228.679 seconds used for training.\n", "787.14 minutes used for training.\n", "Peak reserved memory = 8.832 GB.\n", "Peak reserved memory for training = 2.059 GB.\n", "Peak reserved memory % of max memory = 59.915 %.\n", "Peak reserved memory for training % of max memory = 13.968 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "R8Sd_AqILeYi" }, "source": [ "The training procedure generates both standard training logs and **trackio** logs, which help us monitor the training progress. Example outputs would look like the following:" ] }, { "cell_type": "markdown", "metadata": { "id": "2bPn6gruLf-n" }, "source": [ "" ] }, { "cell_type": "markdown", "metadata": { "id": "ibO4f7tuLboQ" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "e6a3677667ce47bcba55e3e950e446f9", "17adb84604d84cf688a89a21f6cc6150", "a21c1bbd3cd04738a8c96fbfc0c016c6", "65cadde3da7642188f029bb2aceaa7c6", "0404b89e5ce24e76958c72bedc1a95cc", "c52baf990fde40c0873747e827dc6926", "191653e8ce184123a68f26fbf2b78745", "0bb882d400864b249c80132264de2623", "09cbfcf6e51c431798f4e392a81be6d3", "d6521f73f23f42e18ee462a547f251a1" ] }, "id": "itpVDjy0Wbdt", "outputId": "b821c7ed-6c9d-440a-a797-e25291627bef" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "81eBZe-X7daz" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "1d3fbf86d53845beac599c5b231e87ea" ] }, "id": "ZLdaWYzNWbdt", "outputId": "a103b64b-1f6b-4423-c5fd-402f210e6dc3" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "1d3fbf86d53845beac599c5b231e87ea", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading checkpoint shards: 0%| | 0/4 [00:00 and tags. After closing , the assistant MUST provide the final answer in plain text.',\n", " 'role': 'system'},\n", " {'content': \"In 1988, a person's age was equal to the sum of the digits of their birth year. How old was this person?\",\n", " 'role': 'user'}]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = 'AI-MO/NuminaMath-TIR'\n", "test_dataset = load_dataset(dataset_name, split='test[:1%]')\n", "test_dataset = test_dataset.map(make_conversation)\n", "test_dataset = test_dataset.remove_columns(['messages', 'problem'])\n", "test_dataset[0]['prompt']" ] }, { "cell_type": "markdown", "metadata": { "id": "CxKyZwG28BYJ" }, "source": [ "Let's first check what's the output for the base model, without the adapter." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qTPJY96eWbdt", "outputId": "ed02acca-e856-44ec-fa20-c32efd81e018" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "To solve this problem, let's denote the birth year of the person as \\(Y\\) (where \\(Y\\) is a four-digit number) and their age in 1988 as \\(A\\). According to the given condition, their age in 1988 is equal to the sum of the digits of their birth year. \n", "\n", "Since we're looking at the year 1988, the person would be \\(1988 - Y\\) years old in that year. Given the condition:\n", "\n", "\\[1988 - Y = \\text{sum of the digits of } Y\\]\n", "\n", "Let's break down the possible range for \\(Y\\). Since the person's age must be less than or equal to 100 (as the sum of the digits of any four-digit number cannot exceed 36), \\(Y\\) must be between 1989 and 2088.\n", "\n", "We can systematically check each year in this range to find when the condition holds true. However, considering the constraint on age, we can narrow our search significantly. For example, if \\(Y\\) were 1990, the sum of its digits would be 18, which is not a reasonable age. We need\n" ] } ], "source": [ "messages = test_dataset[0]['prompt']\n", "text = tokenizer.apply_chat_template(\n", " messages, add_generation_prompt=True, tokenize=False\n", ")\n", "model_inputs = tokenizer([text], return_tensors=\"pt\").to(base_model.device)\n", "\n", "generated_ids = base_model.generate(\n", " **model_inputs,\n", " max_new_tokens=256\n", ")\n", "output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]\n", "\n", "# Decode and extract model response\n", "generated_text = tokenizer.decode(output_ids, skip_special_tokens=True)\n", "print(generated_text)" ] }, { "cell_type": "markdown", "metadata": { "id": "V9eoUwQS8SIi" }, "source": [ "The base model neither produced reasoning traces nor provided a correct answer. Let's now load the fine-tuned model and check its performance." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "073b351afd264bf0bf23043b37e0d8ce", "3dee429faf4e40b192cabebfe4bf2245" ] }, "id": "CNannsXXWbdt", "outputId": "fc43a5b9-4ec6-43eb-fc34-f26e92434faf" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "073b351afd264bf0bf23043b37e0d8ce", "version_major": 2, "version_minor": 0 }, "text/plain": [ "adapter_config.json: 0.00B [00:00, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3dee429faf4e40b192cabebfe4bf2245", "version_major": 2, "version_minor": 0 }, "text/plain": [ "adapter_model.safetensors: 0%| | 0.00/162M [00:00 I need to find a birth year where the sum of its digits equals the person's age in 1988 \n", "\n", "The person would have been born in 1979, since 1+9+7+9 = 26 and 26 is the age in 1988\n", "\n", "answer: 26\n" ] } ], "source": [ "text = tokenizer.apply_chat_template(\n", " messages, add_generation_prompt=True, tokenize=False\n", ")\n", "model_inputs = tokenizer([text], return_tensors=\"pt\").to(fine_tuned_model.device)\n", "\n", "generated_ids = fine_tuned_model.generate(\n", " **model_inputs,\n", " max_new_tokens=256\n", ")\n", "output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]\n", "\n", "# Decode and extract model response\n", "generated_text = tokenizer.decode(output_ids, skip_special_tokens=True)\n", "print(generated_text)" ] }, { "cell_type": "markdown", "metadata": { "id": "OU-xDHpEEmg9" }, "source": [ "The final answer is correct!" ] }, { "cell_type": "markdown", "metadata": { "id": "XNtBOpRY8a2O" }, "source": [ "## Inference and Serving with vLLM\n", "\n", "You can use Transformer models with **vLLM** to serve them in real-world applications. Learn more [here](https://blog.vllm.ai/2025/04/11/transformers-backend.html)." ] }, { "cell_type": "markdown", "metadata": { "id": "nkhu0uY78lV3" }, "source": [ "### Push Merged Model (for LoRA or QLoRA Training)\n", "\n", "To serve the model via **vLLM**, the repository must contain the merged model (base model + LoRA adapter). Therefore, you need to upload it first." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "NF8ZP9Z-Wbdt", "outputId": "32a5ab71-1f0d-4289-ea12-66f5f75a957b" }, "outputs": [ { "data": { "text/plain": [ "('Qwen2-7B-Instruct-GRPO-merged/tokenizer_config.json',\n", " 'Qwen2-7B-Instruct-GRPO-merged/special_tokens_map.json',\n", " 'Qwen2-7B-Instruct-GRPO-merged/chat_template.jinja',\n", " 'Qwen2-7B-Instruct-GRPO-merged/vocab.json',\n", " 'Qwen2-7B-Instruct-GRPO-merged/merges.txt',\n", " 'Qwen2-7B-Instruct-GRPO-merged/added_tokens.json',\n", " 'Qwen2-7B-Instruct-GRPO-merged/tokenizer.json')" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model_merged = fine_tuned_model.merge_and_unload()\n", "\n", "save_dir = f\"{output_dir}-merged\"\n", "\n", "model_merged.save_pretrained(save_dir)\n", "tokenizer.save_pretrained(save_dir)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "d1a0574cc20046d5876cf31b21955f8b", "7cc2f0ef7ad2494cad572cd898095c00", "475420d92bb54dc08517ffe423b015c3", "a76231aeae5a49979d1e9075b0b3eefb", "b4f469f957134ea9b0e28532fe3caaf1", "637e55736da34f2c9b098222ae07244a", "8157e521017c450a9d2a9e41611405e9", "9746ae4ab0574ed186f898dba3b4b197", "d4b2a8805ec548ea85e0900ff5927574", "0668cd8597f141e89ef38129c6641c1f" ] }, "id": "X5Zci39rWbdt", "outputId": "ca329f99-dc7b-470c-f5d9-39a3eabcb16d" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d1a0574cc20046d5876cf31b21955f8b", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7cc2f0ef7ad2494cad572cd898095c00", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "475420d92bb54dc08517ffe423b015c3", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0002-of-00004.safetensors: 0%| | 612kB / 4.93GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a76231aeae5a49979d1e9075b0b3eefb", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0003-of-00004.safetensors: 0%| | 611kB / 4.33GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b4f469f957134ea9b0e28532fe3caaf1", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0001-of-00004.safetensors: 1%|1 | 50.3MB / 4.88GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "637e55736da34f2c9b098222ae07244a", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0004-of-00004.safetensors: 4%|3 | 41.9MB / 1.09GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "8157e521017c450a9d2a9e41611405e9", "version_major": 2, "version_minor": 0 }, "text/plain": [ "README.md: 0.00B [00:00, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "9746ae4ab0574ed186f898dba3b4b197", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d4b2a8805ec548ea85e0900ff5927574", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "0668cd8597f141e89ef38129c6641c1f", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...RPO-merged/tokenizer.json: 100%|##########| 11.4MB / 11.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" }, "text/plain": [ "CommitInfo(commit_url='https://huggingface.co/sergiopaniego/Qwen2-7B-Instruct-GRPO-merged/commit/b20988444532e79a6915f0b2b6002b5acc2b53e1', commit_message='Upload tokenizer', commit_description='', oid='b20988444532e79a6915f0b2b6002b5acc2b53e1', pr_url=None, repo_url=RepoUrl('https://huggingface.co/sergiopaniego/Qwen2-7B-Instruct-GRPO-merged', endpoint='https://huggingface.co', repo_type='model', repo_id='sergiopaniego/Qwen2-7B-Instruct-GRPO-merged'), pr_revision=None, pr_num=None)" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "model_merged.push_to_hub(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization\n", "tokenizer.push_to_hub(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization" ] }, { "cell_type": "markdown", "metadata": { "id": "DQ00Ivxi8rFu" }, "source": [ "### Performing Inference with vLLM\n", "\n", "Use **vLLM** to run your model and generate text efficiently in real-time. This allows you to test and deploy your fine-tuned models with low latency and high throughput." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "x7L-HIn4Wbdt", "outputId": "afd66093-3525-4590-f834-c0b373e7bb9e" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "INFO 12-11 15:56:09 [utils.py:253] non-default args: {'dtype': torch.float16, 'max_model_len': 256, 'disable_log_stats': True, 'model_impl': 'transformers', 'model': 'sergiopaniego/Qwen2-7B-Instruct-GRPO-merged'}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:104: UserWarning: \n", "Error while fetching `HF_TOKEN` secret value from your vault: 'Requesting secret HF_TOKEN timed out. Secrets can only be fetched when running from the Colab UI.'.\n", "You are not authenticated with the Hugging Face Hub in this notebook.\n", "If the error persists, please let us know by opening an issue on GitHub (https://github.com/huggingface/huggingface_hub/issues/new).\n", " warnings.warn(\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "INFO 12-11 15:56:37 [model.py:631] Resolved architecture: TransformersForCausalLM\n", "WARNING 12-11 15:56:37 [model.py:1971] Casting torch.bfloat16 to torch.float16.\n", "INFO 12-11 15:56:37 [model.py:1745] Using max model len 256\n", "INFO 12-11 15:56:40 [scheduler.py:216] Chunked prefill is enabled with max_num_batched_tokens=8192.\n", "WARNING 12-11 15:56:43 [system_utils.py:103] We must use the `spawn` multiprocessing start method. Overriding VLLM_WORKER_MULTIPROC_METHOD to 'spawn'. See https://docs.vllm.ai/en/latest/usage/troubleshooting.html#python-multiprocessing for more information. Reasons: CUDA is initialized\n", "INFO 12-11 15:57:36 [llm.py:352] Supported tasks: ['generate']\n" ] } ], "source": [ "from vllm import LLM, SamplingParams\n", "from transformers import AutoTokenizer\n", "import torch\n", "\n", "llm = LLM(\n", " model=f\"sergiopaniego/{output_dir}-merged\", # Replace with your HF username or organization\n", " model_impl=\"transformers\", # Select the transformers model implementation\n", " max_model_len=256, # Reduced for efficiency\n", " dtype=torch.float16\n", ")\n", "hf_tokenizer = AutoTokenizer.from_pretrained(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "f0a4f4fb17bf4a698503212296467547", "5be7348f3f324b5b9397c9ad186fb35d" ] }, "id": "ZTpSUqxNWbdt", "outputId": "6a9283bf-d3b7-4e54-c775-4502694b5c6d" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "f0a4f4fb17bf4a698503212296467547", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Adding requests: 0%| | 0/1 [00:00 1988 birth year implies the person was born either in 1979, 1980, 1981, etc. Looking for the one where sum of digits equals age \n", "\n", "The birth year 1979 gives sum of digits 1+9+7+9 = 26\n", "\n", "The person was 26 years old in 1988.\n", "\n", "Answer: The person was 26 years old.\n" ] } ], "source": [ "messages = test_dataset[0]['prompt']\n", "# Alternatively, use llm.chat()\n", "prompt = hf_tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)\n", "\n", "outputs = llm.generate(\n", " {\"prompt\": prompt},\n", " sampling_params=SamplingParams(max_tokens=256),\n", ")\n", "\n", "for o in outputs:\n", " generated_text = o.outputs[0].text\n", " print(generated_text)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/openenv_sudoku_grpo.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "lSR2nwdJg962" }, "source": [ "# OpenEnv Sudoku with GRPO using TRL\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/openenv_sudoku_grpo.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)\n", "\n", "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can train a model that learns to **play Sudoku**, through interaction and reinforcement.\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)\n", "- [OpenEnv](https://github.com/meta-pytorch/OpenEnv)\n", "\n", "An **agentic environment** is a setting where a model can take actions, observe outcomes, and adjust its behavior based on feedback, similar to how humans learn from trial and error.\n", "In this case, the agent interacts with the **Sudoku** environment through the [**OpenEnv**](https://github.com/meta-pytorch/OpenEnv) framework, which standardizes multi-agent and RL-style text environments.\n", "\n", "Sudoku is a classic logic-based puzzle where the objective is to fill a **9×9 grid** so that. Each **row**, **column**, and **3×3 subgrid** contains all digits from **1 to 9** exactly once.\n", "\n", "This structured yet challenging setup makes Sudoku an excellent benchmark for reasoning and decision-making tasks.\n", "\n", "We'll fine-tune a model using **GRPO** (Group Relative Policy Optimization) via TRL.\n", "The training loop follows these steps:\n", "\n", "1. The agent **generates guesses** based on the current game state.\n", "2. The environment **evaluates the guess** and returns structured feedback.\n", "3. The agent **updates its policy** using reward signals to improve future decisions.\n", "\n", "Over time, the model learns to make increasingly valid and efficient Sudoku moves.\n", "\n", "## Install dependencies\n", "\n", "We'll start by installing **TRL**, which automatically includes the main dependencies like **Transformers**. \n", "We'll also install the **OpenEnv** framework (for the environment) via the HF Space we will use as environment server ([openenv/sudoku](https://huggingface.co/spaces/openenv/sudoku)), **trackio** (for logging and monitoring training runs), and **vLLM** (for efficient generation)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "mHmE7GhRKyJj" }, "outputs": [], "source": [ "!pip install -Uq trl[vllm] trackio git+https://huggingface.co/spaces/openenv/sudoku liger-kernel" ] }, { "cell_type": "markdown", "metadata": { "id": "Inxeq6ZGpRno" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "JRd5fGR-KyJk" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "O3kr38TGm_hb" }, "source": [ "## Initialize the OpenEnv TextArena Environment\n", "\n", "Let's begin by setting up the environment that will be used throughout training.\n", "\n", "For this example, we will use the **TextArena** environment provided by **OpenEnv**, which exposes a familiar **Gymnasium-style API** (`reset()`, `step()`, etc.) to simplify interaction and integration with reinforcement learning pipelines.\n", "\n", "Specifically, we will connect to a **remote TextArena instance** that hosts a **Sudoku environment**, available at [openenv/sudoku](https://huggingface.co/spaces/openenv/sudoku).\n", "\n", "This setup allows us to interact with the environment without needing to run the backend locally.\n", "\n", "> ⚠️ **Note:** Hosted environments on the Hugging Face Hub have limited concurrency. \n", "> For improved stability, higher throughput, or parallel experiments, it is recommended to **duplicate the Space into your own account**.\n", "\n", "For more information, refer to the [TRL-OpenEnv documentation](https://huggingface.co/docs/trl/main/en/openenv).\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "P6O03louKyJk" }, "outputs": [], "source": [ "from textarena_env import TextArenaEnv\n", "\n", "space_url = \"https://openenv-sudoku.hf.space\"\n", "client = TextArenaEnv(base_url=space_url)" ] }, { "cell_type": "markdown", "metadata": { "id": "EqfDavDQnD_5" }, "source": [ "## Create Rollout Function with Helpers\n", "\n", "The **rollout function** defines how the agent interacts with the environment during GRPO training.\n", "It is responsible for generating model outputs, collecting feedback (rewards), and returning all the information needed for policy optimization.\n", "\n", "In this setup:\n", "- The function is called automatically by the **GRPOTrainer** at each training step.\n", "- It uses the trainer's `generate_rollout_completions()` method for efficient generation with **vLLM** in colocate mode.\n", "- Each rollout represents a full interaction loop: the model makes guesses, receives feedback from the Sudoku environment, and updates its policy based on reward signals.\n", "\n", "Rewards track different aspects of the agent's performance, while helper functions like `rollout_once` handle a single episode of interaction, keeping the main `rollout_func` clean and modular.\n", "\n", "This modular approach allows GRPO to efficiently sample, evaluate, and improve the model's guessing strategy through reinforcement learning.\n", "\n", "First, we define the `system_prompt` that guides the model's behavior as an expert Sudoku solver with strategic reasoning and structured responses." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "pi1JGoUBKyJk" }, "outputs": [], "source": [ "# @title System prompt (click to expand)\n", "SYSTEM_PROMPT = \"\"\"You are an expert Sudoku player with deep knowledge of logical deduction strategies and number placement techniques.\n", "\n", "## GAME RULES\n", "\n", "1. The puzzle is a 9x9 grid divided into nine 3x3 subgrids (boxes)\n", "2. Some cells are pre-filled with numbers 1-9\n", "3. You must fill in the empty cells (shown as '.') with numbers 1-9\n", "4. Each row must contain numbers 1-9 without repetition\n", "5. Each column must contain numbers 1-9 without repetition\n", "6. Each 3x3 subgrid must contain numbers 1-9 without repetition\n", "7. You cannot overwrite pre-filled cells\n", "8. Invalid moves result in penalties (-1 reward)\n", "\n", "## RESPONSE FORMAT\n", "\n", "**CRITICAL: Output ONLY the move, nothing else. No text, no explanation.**\n", "\n", "Format: [row col number]\n", "\n", "Examples:\n", "- [5 3 7] → places 7 in row 5, column 3\n", "- [1 2 4] → places 4 in row 1, column 2\n", "\n", "## STRATEGIC APPROACH\n", "\n", "Do not repeat the same move twice.\n", "\n", "### Basic Strategies\n", "- **Naked Singles**: If a cell has only one possible candidate, fill it in immediately.\n", "- **Hidden Singles**: If a number can only go in one cell within a row, column, or box, place it there.\n", "- **Scanning**: Look at each row, column, and box to find where specific numbers can go.\n", "\n", "### Intermediate Strategies\n", "- **Naked Pairs/Triples**: When two/three cells in a unit contain only the same candidates, eliminate those from other cells.\n", "- **Hidden Pairs/Triples**: When numbers only appear in specific cells within a unit, those cells can only contain those numbers.\n", "- **Pointing Pairs**: When a candidate in a box is restricted to a single row/column, eliminate it elsewhere.\n", "\n", "### Solving Process\n", "1. Start by scanning the entire grid to identify easy fills (cells with few candidates)\n", "2. Look for rows, columns, or boxes with many numbers already placed\n", "3. Fill all naked singles first\n", "4. Then look for hidden singles in each row, column, and box\n", "5. Apply more advanced techniques as needed\n", "\n", "### Common Pitfalls to Avoid\n", "- Don't guess randomly - Sudoku is pure logic\n", "- Don't overlook any constraint (row, column, or box)\n", "- Don't try to overwrite pre-filled cells\n", "- Don't place invalid numbers (must be 1-9)\n", "- Don't use invalid coordinates (must be 1-9)\n", "- Don't repeat a move that was already made\n", "\n", "## EXAMPLES\n", "\n", "### Example 1: Naked Single\n", "If row 3, column 4 can only contain the number 5:\n", "[3 4 5]\n", "\n", "### Example 2: Hidden Single\n", "If the number 8 can only go in one cell in row 1:\n", "[1 7 8]\n", "\n", "### Example 3: Row Analysis\n", "Row 2 is missing only value 5, and column 8 is the empty cell:\n", "[2 8 5]\n", "\n", "### Example 4: Box Analysis\n", "In the center box, only one cell can contain 9:\n", "[5 5 9]\n", "\n", "## BOARD READING\n", "\n", "The board is displayed as a 9x9 grid:\n", "- Numbers 1-9 are pre-filled or already placed\n", "- Empty cells are shown as '.'\n", "- Rows are labeled R1-R9 (top to bottom)\n", "- Columns are labeled C1-C9 (left to right)\n", "\n", "Example board representation:\n", "```\n", " C1 C2 C3 C4 C5 C6 C7 C8 C9\n", "R1 . 8 9 | 1 . . | . 3 7\n", "R2 2 7 1 | 9 4 3 | 6 . 8\n", "R3 . 6 5 | . 2 7 | 4 9 .\n", " - - - - - - - - - - - - - - - -\n", "R4 . . . | 7 8 . | 9 2 3\n", "R5 . 9 2 | . 5 6 | . . 4\n", "R6 7 3 8 | . . 2 | 1 . .\n", " - - - - - - - - - - - - - - - -\n", "R7 8 4 . | . . 9 | 5 . .\n", "R8 5 . . | 6 . 8 | 3 4 9\n", "R9 9 . 6 | 5 3 4 | 8 7 2\n", "```\n", "\n", "## COORDINATE REFERENCE\n", "\n", "Row indices (top to bottom): 1, 2, 3, 4, 5, 6, 7, 8, 9\n", "Column indices (left to right): 1, 2, 3, 4, 5, 6, 7, 8, 9\n", "\n", "Subgrid layout:\n", "```\n", "Subgrid 1 | Subgrid 2 | Subgrid 3\n", " (R1-R3) (R1-R3) (R1-R3)\n", " (C1-C3) (C4-C6) (C7-C9)\n", "----------+-----------+----------\n", "Subgrid 4 | Subgrid 5 | Subgrid 6\n", " (R4-R6) (R4-R6) (R4-R6)\n", " (C1-C3) (C4-C6) (C7-C9)\n", "----------+-----------+----------\n", "Subgrid 7 | Subgrid 8 | Subgrid 9\n", " (R7-R9) (R7-R9) (R7-R9)\n", " (C1-C3) (C4-C6) (C7-C9)\n", "```\n", "\n", "## IMPORTANT CONSTRAINTS\n", "\n", "- Coordinates are 1-indexed (1-9 for both row and column)\n", "- Numbers must be 1-9\n", "- One move per response\n", "- Must be a valid move (no rule violations)\n", "- Never repeat a previous move\n", "\n", "## YOUR GOAL\n", "\n", "Output ONLY your move in the format [row col number]. No explanation, no reasoning, just the move.\n", "\"\"\"" ] }, { "cell_type": "markdown", "metadata": { "id": "Vi1rFey39GUl" }, "source": [ "Now, let's define the `rollout_func`.\n", "\n", "This function manages the interaction between the model and the Sudoku environment. \n", "For each prompt in the batch, it runs a full episode, collecting both the model's outputs and the corresponding rewards. These results are then used by GRPO to optimize the agent's policy.\n", "\n", "Each game allows the model to make **up to 100 turns**, giving it multiple chances to solve the puzzle.\n", "We have different difficulty levels available: `'easy'`, `'medium'`, and `'hard'`. The level affects the amount of information provided in the prompt. Higher difficulties give less guidance.\n", "\n", "For the **easy** level, the Qwen/Qwen3-1.7B model is sufficient to solve the puzzles efficiently in a Colab notebook.\n", "For **medium** or **hard** levels, a larger or more advanced model would likely be needed." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "wMQQoQ_UKyJl" }, "outputs": [], "source": [ "from trl import GRPOTrainer\n", "\n", "max_turns = 100\n", "debug = False # Activate for detailed logs during training\n", "difficulty=\"easy\"\n", "\n", "def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]:\n", " all_prompt_ids = []\n", " all_completion_ids = []\n", " all_logprobs = []\n", " all_correct = []\n", " all_valid = []\n", " all_empty_cell = []\n", " all_repetition = []\n", " all_progress = []\n", "\n", " for _ in prompts:\n", " episode = rollout_once(\n", " trainer=trainer,\n", " env=client,\n", " tokenizer=trainer.processing_class,\n", " system_prompt=SYSTEM_PROMPT,\n", " max_turns=max_turns,\n", " debug=debug,\n", " difficulty=difficulty,\n", " )\n", " all_prompt_ids.append(episode[\"prompt_ids\"])\n", " all_completion_ids.append(episode[\"completion_ids\"])\n", " all_logprobs.append(episode[\"logprobs\"])\n", " all_correct.append(episode[\"correct_reward\"])\n", " all_valid.append(episode[\"valid_move_reward\"])\n", " all_empty_cell.append(episode[\"empty_cell_reward\"])\n", " all_repetition.append(episode[\"repetition_reward\"])\n", " all_progress.append(episode[\"progress_reward\"])\n", "\n", " return {\n", " \"prompt_ids\": all_prompt_ids,\n", " \"completion_ids\": all_completion_ids,\n", " \"logprobs\": all_logprobs,\n", " \"correct_reward\": all_correct,\n", " \"valid_move_reward\": all_valid,\n", " \"empty_cell_reward\": all_empty_cell,\n", " \"repetition_reward\": all_repetition,\n", " \"progress_reward\": all_progress,\n", " }\n" ] }, { "cell_type": "markdown", "metadata": { "id": "ioUHdIxr9ZQO" }, "source": [ "### Define `rollout_once`\n", "\n", "The `rollout_once` function runs **a single interaction loop** between the model and the Sudoku environment using the trainer's generation method. \n", "It executes one mini-episode, from generating a guess to receiving and processing feedback.\n", "\n", "Step-by-step:\n", "\n", "1. **Environment reset:** Start a new game session and initialize the observation.\n", "2. **Prompt construction:** Combine the system prompt, current state, and user messages to form the model input.\n", "3. **Generation:** Use `trl.experimental.openenv.generate_rollout_completions()` to efficiently produce the model's guess.\n", "4. **Feedback extraction:** Parse the environment's response with helpers like `extract_sudoku_move()` and `extract_feedback()`.\n", "5. **Reward calculation:** Compute rewards based on correctness, valid moves, empty cell moves, repeated moves, and progress.\n", "6. **Return structured rollout data:** Includes prompt and completion IDs, log probabilities, and all reward components.\n", "\n", "This design allows each episode to be processed independently while providing detailed feedback for the **GRPO training loop**." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "AZim6XzEKyJl" }, "outputs": [], "source": [ "from trl.experimental.openenv import generate_rollout_completions\n", "from textarena_env import TextArenaAction\n", "from transformers import AutoTokenizer\n", "from collections import defaultdict\n", "\n", "\n", "def rollout_once(\n", " trainer: GRPOTrainer,\n", " env: TextArenaEnv,\n", " tokenizer: AutoTokenizer,\n", " system_prompt: str,\n", " max_turns: int,\n", " debug: bool = False,\n", " difficulty: str = \"hard\",\n", ") -> dict[str, list]:\n", " result = env.reset()\n", " observation = result.observation\n", "\n", " # Only store the LAST turn for backprop (much more efficient!)\n", " last_turn_data: dict | None = None\n", "\n", " valid_move_scores: list[float] = []\n", " empty_cell_scores: list[float] = []\n", " correct_scores: list[float] = []\n", " repetition_scores: list[float] = []\n", "\n", " move_counts: defaultdict[str, int] = defaultdict(int)\n", "\n", " # Track successful and failed moves for summary\n", " successful_moves: list[str] = []\n", " failed_moves: list[str] = []\n", "\n", " # Extract initial board state\n", " last_board_state = \"\"\n", " initial_filled = 0\n", " for message in observation.messages:\n", " if message.content and is_valid_board_state(message.content):\n", " last_board_state = message.content\n", " initial_filled = count_filled_cells(last_board_state)\n", " break\n", "\n", " max_filled = initial_filled # Track max progress\n", "\n", " for turn in range(max_turns):\n", " if result.done:\n", " break\n", "\n", " # Build COMPACT prompt (saves tokens!)\n", " user_prompt = make_compact_prompt(\n", " board=last_board_state,\n", " step=turn + 1,\n", " successful_moves=successful_moves,\n", " failed_moves=failed_moves,\n", " difficulty=difficulty,\n", " )\n", " messages = [\n", " {\"role\": \"system\", \"content\": system_prompt},\n", " {\"role\": \"user\", \"content\": user_prompt},\n", " ]\n", " prompt_text = tokenizer.apply_chat_template(\n", " messages, add_generation_prompt=True, tokenize=False, enable_thinking=False # `enable_thinking` is usable for the current model but could need to be updated for other models\n", " )\n", "\n", " if debug:\n", " print(f\"\\n{'=' * 60}\")\n", " print(f\"STEP {turn + 1}\")\n", " print(f\"{'=' * 60}\")\n", " print(f\"USER PROMPT:\\n{user_prompt}\")\n", " print(f\"{'=' * 60}\")\n", "\n", " # Generate\n", " rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0]\n", "\n", " # Store ONLY this turn's data (replace previous)\n", " last_turn_data = {\n", " \"prompt_ids\": rollout_outputs[\"prompt_ids\"],\n", " \"completion_ids\": rollout_outputs[\"completion_ids\"],\n", " \"logprobs\": rollout_outputs[\"logprobs\"],\n", " }\n", "\n", " if debug:\n", " step_tokens = len(rollout_outputs[\"prompt_ids\"]) + len(rollout_outputs[\"completion_ids\"])\n", " print(f\"TOKENS: this_step={step_tokens} (only last turn used for backprop)\")\n", "\n", " completion_text = rollout_outputs.get(\"text\") or tokenizer.decode(\n", " rollout_outputs[\"completion_ids\"], skip_special_tokens=True\n", " )\n", "\n", " # Extract move\n", " move = extract_sudoku_move(completion_text)\n", "\n", " if debug:\n", " print(f\"MODEL OUTPUT: {completion_text}\")\n", " print(f\"EXTRACTED MOVE: {move}\")\n", "\n", " # Step environment\n", " result = env.step(TextArenaAction(message=move))\n", " observation = result.observation\n", " correct_score = float(result.reward or 0.0)\n", "\n", " # Get feedback\n", " feedback = extract_feedback(observation)\n", "\n", " # Get environment response\n", " env_response = \"\"\n", " for msg in observation.messages:\n", " if msg.sender_id == -1: # Environment message\n", " env_response = msg.content\n", " break\n", "\n", " if debug:\n", " print(\n", " f\"ENV RESPONSE: {env_response[:200]}...\"\n", " if len(env_response) > 200\n", " else f\"ENV RESPONSE: {env_response}\"\n", " )\n", " print(f\"VALID: {feedback['valid_move']}, WARNING: {feedback['got_warning']}, REWARD: {correct_score}\")\n", "\n", " # Calculate empty_cell_score\n", " if last_board_state and move:\n", " targets_empty = check_move_targets_empty_cell(move, last_board_state)\n", " empty_cell_score = 1.0 if targets_empty else -1.0\n", " else:\n", " empty_cell_score = 0.0\n", "\n", " # Calculate valid_move_score and repetition_score\n", " is_new_move = move_counts[move] == 0\n", " repetition_count = move_counts[move]\n", " move_counts[move] += 1\n", "\n", " # Exponential penalty for repetitions: -2^(n-1) capped at -10\n", " # 1st repeat: -1, 2nd: -2, 3rd: -4, 4th+: -10 (capped)\n", " if repetition_count > 0:\n", " repetition_score = -min(2 ** (repetition_count - 1), 10.0)\n", " else:\n", " repetition_score = 0.0\n", "\n", " if debug:\n", " print(\n", " f\"SCORES: empty_cell={empty_cell_score}, is_new={is_new_move}, repetitions={repetition_count}, rep_penalty={repetition_score}\"\n", " )\n", "\n", " if not debug:\n", " print(f\"Step {turn + 1}: {move}\")\n", "\n", " if feedback[\"valid_move\"] and is_new_move:\n", " valid_move_score = 1.0\n", " if move:\n", " successful_moves.append(move) # Track for summary\n", " elif feedback[\"got_warning\"]:\n", " valid_move_score = -0.5\n", " if move:\n", " failed_moves.append(move) # Track for summary\n", " else:\n", " valid_move_score = 0.0\n", "\n", " # Update board state and track progress\n", " if feedback[\"board_state\"] and is_valid_board_state(feedback[\"board_state\"]):\n", " last_board_state = feedback[\"board_state\"]\n", " current_filled = count_filled_cells(last_board_state)\n", " if current_filled > max_filled:\n", " max_filled = current_filled\n", "\n", " valid_move_scores.append(valid_move_score)\n", " empty_cell_scores.append(empty_cell_score)\n", " correct_scores.append(correct_score)\n", " repetition_scores.append(repetition_score)\n", "\n", " # Aggregate rewards\n", " correct_reward = correct_scores[-1] if correct_scores else 0.0\n", " valid_move_reward = sum(valid_move_scores) / len(valid_move_scores) if valid_move_scores else 0.0\n", " empty_cell_reward = sum(empty_cell_scores) / len(empty_cell_scores) if empty_cell_scores else 0.0\n", " repetition_reward = sum(repetition_scores) / len(repetition_scores) if repetition_scores else 0.0\n", "\n", " # Progress reward: how many cells we filled beyond initial state (normalized to 0-1)\n", " # 81 total cells, so (max_filled - initial_filled) / (81 - initial_filled) gives progress\n", " remaining_to_fill = 81 - initial_filled\n", " if remaining_to_fill > 0:\n", " progress_reward = (max_filled - initial_filled) / remaining_to_fill\n", " else:\n", " progress_reward = 1.0 # Already complete\n", "\n", " # Use ONLY last turn for backpropagation (much more efficient!)\n", " if last_turn_data:\n", " prompt_ids = last_turn_data[\"prompt_ids\"]\n", " completion_ids = last_turn_data[\"completion_ids\"]\n", " logprobs = last_turn_data[\"logprobs\"]\n", " else:\n", " prompt_ids = []\n", " completion_ids = []\n", " logprobs = []\n", "\n", " total_tokens = len(prompt_ids) + len(completion_ids)\n", " cells_filled = max_filled - initial_filled\n", " print(\n", " f\"Episode: empty_cell={empty_cell_reward:.2f}, valid={valid_move_reward:.2f}, \"\n", " f\"repetition={repetition_reward:.2f}, progress={progress_reward:.2f} ({cells_filled} cells), \"\n", " f\"correct={correct_reward:.2f}, tokens={total_tokens}\"\n", " )\n", "\n", " return {\n", " \"prompt_ids\": prompt_ids,\n", " \"completion_ids\": completion_ids,\n", " \"logprobs\": logprobs,\n", " \"correct_reward\": correct_reward,\n", " \"valid_move_reward\": valid_move_reward,\n", " \"empty_cell_reward\": empty_cell_reward,\n", " \"repetition_reward\": repetition_reward,\n", " \"progress_reward\": progress_reward,\n", " }" ] }, { "cell_type": "markdown", "metadata": { "id": "MDJKMQ__8qzj" }, "source": [ "### Helper Functions\n", "\n", "These utility functions are used within `rollout_once` to process the environment and model outputs:\n", "\n", "- **`extract_sudoku_move`**: Extract a Sudoku move `[row, col, number]` from text. \n", "- **`is_valid_board_state`**: Check if a string represents a valid Sudoku board. \n", "- **`parse_board`**: Convert a board string into a 9×9 grid (with `0` for empty cells). \n", "- **`count_filled_cells`**: Count the number of filled cells in the board. \n", "- **`get_valid_numbers`**: Get the valid numbers for a specific cell according to Sudoku rules. \n", "- **`extract_empty_cells_with_candidates`**: Identify empty cells along with their valid candidate numbers. \n", "- **`extract_empty_cells`**: List all empty cells `(row, col)` from a board string. \n", "- **`extract_board_only`**: Extract just the Sudoku grid from a message. \n", "- **`make_compact_prompt`**: Create a concise prompt with only essential information to save tokens. \n", "- **`check_move_targets_empty_cell`**: Verify if a proposed move targets an empty cell on the board. \n", "- **`extract_feedback`**: Extract structured feedback from the environment's observation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0f9RqHh7KyJl" }, "outputs": [], "source": [ "# @title Helpers (click to expand)\n", "import re\n", "\n", "def extract_sudoku_move(text: str) -> str:\n", " \"\"\"Extract a Sudoku move [row col number] from text.\"\"\"\n", " # Try with spaces\n", " match = re.search(r\"\\[(\\d)\\s+(\\d)\\s+(\\d)\\]\", text)\n", " if match:\n", " row, col, num = match.groups()\n", " return f\"[{row} {col} {num}]\"\n", "\n", " # Try without spaces\n", " match = re.search(r\"\\[(\\d)(\\d)(\\d)\\]\", text)\n", " if match:\n", " row, col, num = match.groups()\n", " return f\"[{row} {col} {num}]\"\n", "\n", " return \"\" # Handled by the environment: missing/invalid moves trigger a \"wrong movement\" message affecting rewards\n", "\n", "\n", "def is_valid_board_state(board_str: str) -> bool:\n", " \"\"\"Check if the string contains an actual Sudoku board.\"\"\"\n", " return \"R1\" in board_str and \"R9\" in board_str and \"|\" in board_str\n", "\n", "\n", "def parse_board(board_str: str) -> list[list[int]]:\n", " \"\"\"Parse board string into 9x9 grid (0 = empty).\"\"\"\n", " grid = [[0] * 9 for _ in range(9)]\n", " if not is_valid_board_state(board_str):\n", " return grid\n", "\n", " for line in board_str.split(\"\\n\"):\n", " line_stripped = line.strip()\n", " if line_stripped and line_stripped[0] == \"R\" and len(line_stripped) > 1 and line_stripped[1].isdigit():\n", " row = int(line_stripped[1]) - 1 # 0-indexed\n", " cell_part = line_stripped[2:]\n", " col = 0\n", " for char in cell_part:\n", " if char == \".\":\n", " grid[row][col] = 0\n", " col += 1\n", " elif char.isdigit():\n", " grid[row][col] = int(char)\n", " col += 1\n", " return grid\n", "\n", "\n", "def count_filled_cells(board_str: str) -> int:\n", " \"\"\"Count the number of filled cells in the board.\"\"\"\n", " if not is_valid_board_state(board_str):\n", " return 0\n", " grid = parse_board(board_str)\n", " return sum(1 for row in grid for cell in row if cell != 0)\n", "\n", "\n", "def get_valid_numbers(grid: list[list[int]], row: int, col: int) -> set[int]:\n", " \"\"\"Get valid numbers for a cell based on Sudoku rules.\"\"\"\n", " if grid[row][col] != 0:\n", " return set()\n", "\n", " used = set()\n", "\n", " # Check row\n", " for c in range(9):\n", " if grid[row][c] != 0:\n", " used.add(grid[row][c])\n", "\n", " # Check column\n", " for r in range(9):\n", " if grid[r][col] != 0:\n", " used.add(grid[r][col])\n", "\n", " # Check 3x3 box\n", " box_row, box_col = 3 * (row // 3), 3 * (col // 3)\n", " for r in range(box_row, box_row + 3):\n", " for c in range(box_col, box_col + 3):\n", " if grid[r][c] != 0:\n", " used.add(grid[r][c])\n", "\n", " return set(range(1, 10)) - used\n", "\n", "\n", "def extract_empty_cells_with_candidates(\n", " board_str: str, sort_by_difficulty: bool = True\n", ") -> list[tuple[int, int, set[int]]]:\n", " \"\"\"Extract empty cells with their valid candidate numbers.\n", "\n", " Args:\n", " sort_by_difficulty: If True, sort by number of candidates (easiest first).\n", " If False, keep natural order (top-left to bottom-right).\n", " \"\"\"\n", " grid = parse_board(board_str)\n", " cells_with_candidates = []\n", "\n", " for row in range(9):\n", " for col in range(9):\n", " if grid[row][col] == 0:\n", " candidates = get_valid_numbers(grid, row, col)\n", " cells_with_candidates.append((row + 1, col + 1, candidates)) # 1-indexed\n", "\n", " if sort_by_difficulty:\n", " # Sort by number of candidates (easiest first = naked singles)\n", " cells_with_candidates.sort(key=lambda x: len(x[2]))\n", "\n", " return cells_with_candidates\n", "\n", "\n", "def extract_empty_cells(board_str: str) -> list[tuple[int, int]]:\n", " \"\"\"Extract list of empty cells (row, col) from board string.\"\"\"\n", " empty_cells = []\n", " if not is_valid_board_state(board_str):\n", " return empty_cells\n", "\n", " for line in board_str.split(\"\\n\"):\n", " line_stripped = line.strip()\n", " if line_stripped and line_stripped[0] == \"R\" and len(line_stripped) > 1 and line_stripped[1].isdigit():\n", " row = int(line_stripped[1])\n", " cell_part = line_stripped[2:]\n", " col = 0\n", " for char in cell_part:\n", " if char == \".\":\n", " col += 1\n", " empty_cells.append((row, col))\n", " elif char.isdigit():\n", " col += 1\n", " return empty_cells\n", "\n", "\n", "def extract_board_only(text: str) -> str:\n", " \"\"\"Extract just the Sudoku grid from a message.\"\"\"\n", " if not text:\n", " return \"\"\n", "\n", " lines = text.split(\"\\n\")\n", " board_lines = []\n", " in_board = False\n", "\n", " for line in lines:\n", " stripped = line.strip()\n", " if stripped.startswith(\"C1\") or (\n", " stripped and stripped[0] == \"R\" and len(stripped) > 1 and stripped[1].isdigit()\n", " ):\n", " in_board = True\n", " if in_board and (stripped.startswith(\"-\") or stripped.startswith(\"R\") or stripped.startswith(\"C1\")):\n", " board_lines.append(line)\n", " elif (\n", " in_board\n", " and stripped\n", " and not stripped.startswith(\"-\")\n", " and not (stripped[0] == \"R\" and len(stripped) > 1 and stripped[1].isdigit())\n", " ):\n", " break\n", "\n", " return \"\\n\".join(board_lines) if board_lines else \"\"\n", "\n", "\n", "def make_compact_prompt(\n", " board: str,\n", " step: int,\n", " successful_moves: list[str],\n", " failed_moves: list[str],\n", " difficulty: str = \"hard\",\n", ") -> str:\n", " \"\"\"Create a compact prompt with only essential info (saves tokens!).\n", "\n", " Args:\n", " difficulty: Training difficulty level:\n", " - \"easy\": Show guaranteed moves (naked singles) + other options\n", " - \"medium\": Only show other options (hints where to look, not exact answers)\n", " - \"hard\": No hints (model must learn Sudoku rules by itself)\n", " \"\"\"\n", "\n", " # Summary line\n", " cells_filled = len(successful_moves)\n", " summary = f\"Step {step}. Progress: {cells_filled} cells filled.\"\n", "\n", " # Board (only show the grid, stripped down)\n", " board_only = extract_board_only(board) if board else \"No board available.\"\n", "\n", " # Moves already tried (for learning what NOT to do)\n", " tried_moves_hint = \"\"\n", " all_tried = successful_moves + failed_moves\n", " if all_tried:\n", " tried_moves_hint = f\"\\n\\n⚠️ MOVES ALREADY TRIED (do not repeat): {', '.join(all_tried)}\"\n", "\n", " # Hints based on difficulty\n", " hints = \"\"\n", " if difficulty == \"easy\" and board:\n", " # Easy: sorted by difficulty, show guaranteed moves + other easy options\n", " cells_with_candidates = extract_empty_cells_with_candidates(board, sort_by_difficulty=True)\n", " if cells_with_candidates:\n", " guaranteed = []\n", " other_hints = []\n", " for row, col, candidates in cells_with_candidates[:10]:\n", " if len(candidates) == 1:\n", " num = list(candidates)[0]\n", " guaranteed.append(f\"[{row} {col} {num}]\")\n", " elif len(candidates) <= 3:\n", " nums = \",\".join(str(n) for n in sorted(candidates))\n", " other_hints.append(f\"({row},{col})→{nums}\")\n", "\n", " if guaranteed:\n", " hints = f\"\\n\\n🎯 GUARANTEED MOVES: {', '.join(guaranteed[:5])}\"\n", " if other_hints:\n", " hints += f\"\\nOther options: {' | '.join(other_hints[:5])}\"\n", "\n", " elif difficulty == \"medium\" and board:\n", " # Medium: NOT sorted, just show empty cells with candidates (no ordering hints)\n", " cells_with_candidates = extract_empty_cells_with_candidates(board, sort_by_difficulty=False)\n", " if cells_with_candidates:\n", " cell_hints = []\n", " for row, col, candidates in cells_with_candidates[:10]:\n", " nums = \",\".join(str(n) for n in sorted(candidates))\n", " cell_hints.append(f\"({row},{col})→{nums}\")\n", " if cell_hints:\n", " hints = f\"\\n\\nEmpty cells: {' | '.join(cell_hints)}\"\n", "\n", " return f\"{summary}\\n\\nBoard:\\n{board_only}{tried_moves_hint}{hints}\\n\\nYour move:\"\n", "\n", "\n", "def check_move_targets_empty_cell(move: str, board_str: str) -> bool:\n", " \"\"\"Check if the move targets an empty cell on the board.\"\"\"\n", " if not move or not board_str:\n", " return False\n", "\n", " match = re.search(r\"\\[(\\d)\\s+(\\d)\\s+(\\d)\\]\", move)\n", " if not match:\n", " return False\n", "\n", " row, col = int(match.group(1)), int(match.group(2))\n", " empty_cells = extract_empty_cells(board_str)\n", " return (row, col) in empty_cells\n", "\n", "\n", "def extract_feedback(observation) -> dict:\n", " \"\"\"Extract feedback from environment observation.\"\"\"\n", " feedback = {\"valid_move\": True, \"got_warning\": False, \"board_state\": \"\"}\n", "\n", " if not observation or not observation.messages:\n", " return feedback\n", "\n", " for message in observation.messages:\n", " content = message.content.lower() if message.content else \"\"\n", "\n", " if any(kw in content for kw in [\"invalid\", \"error\", \"cannot\", \"already\", \"violation\", \"lost\"]):\n", " feedback[\"valid_move\"] = False\n", " if \"please resubmit\" in content or \"avoid penalties\" in content:\n", " feedback[\"got_warning\"] = True\n", "\n", " if message.content and \"|\" in message.content and \"R1\" in message.content:\n", " feedback[\"board_state\"] = message.content\n", "\n", " return feedback" ] }, { "cell_type": "markdown", "metadata": { "id": "Oek3JhcWnKhw" }, "source": [ "## Define Reward Functions\n", "\n", "To guide the agent's learning, we define reward functions that convert the environment's feedback into numeric signals.\n", "Each function captures a specific aspect of performance in the **Sudoku** game:\n", "\n", "- **`reward_empty_cell`**: Reward for targeting empty cells, encouraging the agent to pick valid positions first.\n", "- **`reward_valid_moves`**: Reward for making moves that comply with Sudoku rules.\n", "- **`reward_correct`**: Reward for correctly placing numbers, contributing to solving the puzzle.\n", "- **`reward_repetition`**: Penalty for repeating moves in the same cell.\n", "- **`reward_progress`**: Reward for filling more cells on the board, indicating overall progress." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "TPe4XL89KyJl" }, "outputs": [], "source": [ "def reward_empty_cell(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Reward for targeting empty cells (learn to pick valid positions first).\"\"\"\n", " rewards = kwargs.get(\"empty_cell_reward\")\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def reward_valid_moves(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Reward for making valid moves.\"\"\"\n", " rewards = kwargs.get(\"valid_move_reward\")\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def reward_correct(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Reward for solving the puzzle.\"\"\"\n", " rewards = kwargs.get(\"correct_reward\")\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def reward_repetition(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Penalty for repeating moves.\"\"\"\n", " rewards = kwargs.get(\"repetition_reward\")\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def reward_progress(completions: list[str], **kwargs) -> list[float]:\n", " \"\"\"Reward for filling more cells in the board.\"\"\"\n", " rewards = kwargs.get(\"progress_reward\")\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]" ] }, { "cell_type": "markdown", "metadata": { "id": "66ZsrLplm07U" }, "source": [ "## Load the Custom Dataset\n", "\n", "The dataset is built using repeated prompts to control the total number of training episodes.\n", "\n", "Each entry in the dataset triggers **one rollout episode** during training. \n", "The `dataset_prompt` provides the initial instruction to the model at the start of each episode, ensuring consistent guidance and context for task execution." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zV7C_t1GKyJm" }, "outputs": [], "source": [ "from datasets import Dataset\n", "\n", "dataset_prompt = \"Play Sudoku like an expert.\"\n", "dataset_size = 30\n", "\n", "dataset = Dataset.from_dict({\"prompt\": [dataset_prompt] * dataset_size})" ] }, { "cell_type": "markdown", "metadata": { "id": "-mvka-96m3I7" }, "source": [ "## Fine-tune using TRL and the GRPOTrainer\n", "\n", "The next step is to define the GRPOConfig, which sets all key training parameters.\n", "\n", "This configuration determines how the model interacts with vLLM, handles memory and computation, and records training metrics and logs for monitoring the fine-tuning process." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4BP-aBcVKyJm" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"sudoku-grpo-qwen3\"\n", "\n", "grpo_config = GRPOConfig(\n", " use_vllm=True, # Use vLLM engine for fast and efficient inference\n", " vllm_mode=\"colocate\", # Run vLLM generation on the same GPU as training\n", " vllm_gpu_memory_utilization=0.1, # Fraction of GPU memory allocated to vLLM\n", " vllm_max_model_length=2560, # Maximum context length for vLLM generations\n", "\n", " output_dir=output_dir, # Directory to save model checkpoints and logs\n", " num_train_epochs=1, # Number of training epochs\n", " learning_rate=5e-6, # Initial learning rate\n", "\n", " #weight_decay=args.weight_decay, # Optional weight decay for optimizer\n", " gradient_accumulation_steps=8, # Accumulate gradients over multiple steps to simulate larger batch size\n", " per_device_train_batch_size=1, # Batch size per device (GPU)\n", " warmup_steps=20, # Number of warmup steps for learning rate scheduler\n", " num_generations=8, # Number of rollouts generated per prompt\n", " max_completion_length=8, # Maximum length of generated completions\n", "\n", " logging_steps=1, # Log metrics every N steps\n", " save_strategy=\"steps\", # Save checkpoints based on steps\n", " save_steps=10, # Save every N steps\n", "\n", " report_to=\"trackio\", # Reporting backend for tracking experiments\n", " trackio_space_id=output_dir, # Trackio space ID to log metrics\n", "\n", " use_liger_kernel=False, # Enable Liger kernel optimizations for faster training\n", " # chat_template_kwargs={\"enable_thinking\": False}, # Optional template args for model reasoning. We manage this in the rollout function\n", "\n", " temperature=0.8,\n", " top_k=10,\n", "\n", " model_init_kwargs={\n", " \"use_cache\": False,\n", " }\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "a1taGmD--0Y4" }, "source": [ "## Create `GRPOTrainer` and Start Training\n", "\n", "Next, we initialize the `GRPOTrainer`, which handles the full reinforcement learning loop.\n", "\n", "It requires the **model**, **reward functions**, **rollout function**, and **dataset** defined earlier. \n", "Here, we use **Qwen/Qwen3-1.7B**, a smaller version of the Qwen3 models. This model is sufficient for training on the \"easy\" difficulty Sudoku setting. \n", "For \"medium\" or \"hard\" difficulty, a larger model would be needed, but this setup fits well in Colab with the current configuration.\n", "\n", "The trainer coordinates:\n", "- Interaction between the model and the environment \n", "- Application of reward signals \n", "- Policy updates based on feedback\n", "\n", "Finally, calling `trainer.train()` starts the fine-tuning process, allowing the model to learn to solve Sudoku through repeated feedback and iteration." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "O-aKk1EwKyJm" }, "outputs": [], "source": [ "model_name = \"Qwen/Qwen3-1.7B\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "c75c6199d00d42b88a0ef49f650317bf", "114a42d7d0a74a7a81dad02c21cf41b2" ] }, "id": "cQP77cFYKyJm", "outputId": "8ec8a2c5-6e64-4c88-a99b-3b54e2f0f1c5" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "c75c6199d00d42b88a0ef49f650317bf", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading checkpoint shards: 0%| | 0/2 [00:00" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Created new run: sergiopaniego-1767361842\n", "Step 1: [1 1 6]\n", "Step 2: [2 7 4]\n", "Step 3: [3 4 8]\n", "Step 4: [3 3 2]\n", "Step 5: [4 8 6]\n", "Step 6: [4 8 6]\n", "Episode: empty_cell=1.00, valid=0.58, repetition=-0.17, progress=0.13 (4 cells), correct=-1.00, tokens=1860\n", "Step 1: [1 1 7]\n", "Step 2: [2 4 6]\n", "Step 3: [1 7 8]\n", "Step 4: [2 4 6]\n", "Step 5: [2 4 4]\n", "Step 6: [2 4 6]\n", "Step 7: [2 4 6]\n", "Episode: empty_cell=0.43, valid=0.21, repetition=-1.00, progress=0.10 (3 cells), correct=-1.00, tokens=1866\n", "Step 1: [1 1 2]\n", "Step 2: [1 1 2]\n", "Episode: empty_cell=-1.00, valid=-0.25, repetition=-0.50, progress=0.00 (0 cells), correct=-1.00, tokens=1826\n", "\n", "# ... Output truncated for readability (see Trackio dashboard for full logs) ...\n", "\n", "Step 1: [1 7 6]\n", "Step 2: [1 9 2]\n", "Step 3: [2 6 5]\n", "Step 4: [2 1 3]\n", "Step 5: [2 2 2]\n", "Step 6: [3 1 4]\n", "Step 7: [3 2 6]\n", "Step 8: [2 9 7]\n", "Step 9: [3 4 8]\n", "Step 10: [3 8 9]\n", "Step 11: [1 3 5]\n", "Step 12: [2 9 4]\n", "Step 13: [4 3 8]\n", "Step 14: [3 8 4]\n", "Step 15: [2 9 4]\n", "Episode: empty_cell=0.60, valid=0.73, repetition=-0.07, progress=0.40 (12 cells), correct=-1.00, tokens=1931\n", "Step 1: [2 8 1]\n", "Step 2: [2 5 2]\n", "Step 3: [3 4 6]\n", "Step 4: [1 5 1]\n", "Step 5: [2 6 4]\n", "Step 6: [3 7 4]\n", "Step 7: [4 3 6]\n", "Step 8: [5 1 2]\n", "Step 9: [1 1 4]\n", "Step 10: [4 3 6]\n", "Step 11: [4 3 6]\n", "Step 12: [4 3 6]\n", "Step 13: [4 3 6]\n", "Step 14: [4 1 9]\n", "Step 15: [4 2 4]\n", "Step 16: [7 8 5]\n", "Step 17: [7 8 5]\n", "Episode: empty_cell=0.53, valid=0.62, repetition=-0.94, progress=0.37 (11 cells), correct=-1.00, tokens=1916\n" ] }, { "data": { "text/html": [ "\n", "

\n", " \n", " \n", " [30/30 26:13, Epoch 1/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
1-0.113800
2-0.001800
3-0.051300
4-0.012800
50.012200
60.045600
7-0.104800
8-0.093600
90.182400
10-0.027000
110.042300
12-0.052400
13-0.100100
14-0.074400
15-0.105500
160.125200
17-0.016900
180.119100
190.081800
200.003300
210.024400
22-0.038700
230.000000
240.000000
250.000000
260.000000
270.000000
280.000000
290.000000
300.000000

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Step 1: [1 3 8]\n", "Step 2: [1 9 2]\n", "Step 3: [3 4 2]\n", "Step 4: [3 8 5]\n", "Step 5: [1 2 4]\n", "Step 6: [7 5 1]\n", "Step 7: [7 5 1]\n", "Episode: empty_cell=0.43, valid=0.64, repetition=-0.14, progress=0.17 (5 cells), correct=-1.00, tokens=1881\n", "Step 1: [1 2 9]\n", "Step 2: [1 6 5]\n", "Step 3: [2 8 9]\n", "Step 4: [2 9 7]\n", "Step 5: [3 4 6]\n", "Step 6: [3 3 3]\n", "Step 7: [3 6 2]\n", "Step 8: [3 5 9]\n", "Step 9: [4 7 8]\n", "Step 10: [5 2 2]\n", "Step 11: [5 1 9]\n", "Step 12: [3 4 6]\n", "Step 13: [4 7 1]\n", "Step 14: [5 3 7]\n", "Step 15: [5 5 6]\n", "Step 16: [6 8 4]\n", "Step 17: [6 1 5]\n", "Step 18: [6 5 7]\n", "Step 19: [6 6 1]\n", "Step 20: [7 2 4]\n", "Step 21: [7 3 8]\n", "Step 22: [7 6 6]\n", "Step 23: [7 7 7]\n", "Step 24: [8 3 5]\n", "Step 25: [8 5 2]\n", "Step 26: [8 6 4]\n", "Step 27: [8 9 1]\n", "Step 28: [9 1 2]\n", "Step 29: [9 2 3]\n", "Step 30: [9 3 9]\n", "Step 31: [9 4 8]\n", "Step 32: [9 7 4]\n", "Episode: empty_cell=0.88, valid=0.92, repetition=-0.03, progress=1.00 (30 cells), correct=1.00, tokens=2035\n", "\n", "# ... Output truncated for readability (see Trackio dashboard for full logs) ...\n", "\n", "Step 1: [3 6 4]\n", "Step 2: [2 3 7]\n", "Step 3: [4 9 2]\n", "Step 4: [5 4 7]\n", "Step 5: [3 9 8]\n", "Step 6: [4 6 9]\n", "Step 7: [5 5 1]\n", "Step 8: [5 6 2]\n", "Step 9: [6 3 2]\n", "Step 10: [6 8 8]\n", "Step 11: [5 8 5]\n", "Step 12: [5 2 8]\n", "Step 13: [5 1 9]\n", "Step 14: [6 7 6]\n", "Step 15: [6 5 4]\n", "Step 16: [4 5 6]\n", "Step 17: [6 4 3]\n", "Step 18: [7 4 4]\n", "Step 19: [7 7 8]\n", "Step 20: [7 1 3]\n", "Step 21: [9 2 6]\n", "Step 22: [2 2 4]\n", "Step 23: [3 7 7]\n", "Step 24: [4 1 4]\n", "Step 25: [4 2 5]\n", "Step 26: [9 1 8]\n", "Step 27: [1 2 3]\n", "Step 28: [2 1 6]\n", "Step 29: [1 7 4]\n", "Step 30: [9 7 9]\n", "Episode: empty_cell=1.00, valid=1.00, repetition=0.00, progress=1.00 (30 cells), correct=1.00, tokens=2028\n", "Step 1: [3 3 7]\n", "Step 2: [2 1 9]\n", "Step 3: [3 4 1]\n", "Step 4: [3 6 2]\n", "Step 5: [4 3 1]\n", "Step 6: [4 2 6]\n", "Step 7: [4 7 8]\n", "Step 8: [4 6 5]\n", "Step 9: [4 8 7]\n", "Step 10: [3 8 6]\n", "Step 11: [2 5 8]\n", "Step 12: [6 5 7]\n", "Step 13: [6 1 8]\n", "Step 14: [5 7 3]\n", "Step 15: [7 6 9]\n", "Step 16: [7 7 2]\n", "Step 17: [6 6 3]\n", "Step 18: [8 2 4]\n", "Step 19: [8 4 5]\n", "Step 20: [8 6 1]\n", "Step 21: [5 6 8]\n", "Step 22: [5 4 6]\n", "Step 23: [5 5 1]\n", "Step 24: [8 5 2]\n", "Step 25: [9 4 8]\n", "Step 26: [9 5 6]\n", "Step 27: [4 5 4]\n", "Step 28: [1 9 8]\n", "Step 29: [7 9 7]\n", "Episode: empty_cell=1.00, valid=1.00, repetition=0.00, progress=1.00 (29 cells), correct=1.00, tokens=2020\n", "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "gF-mr-gfAtkp" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qM8tW2pdKyJm" }, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_training = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "training_memory_percentage = round(used_memory_for_training / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_training} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {training_memory_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "BZj4IG9ZBAix" }, "source": [ "In this step, the fine-tuned model is saved locally and uploaded to the Hugging Face Hub using the configured account credentials." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "xJV-NZTmKyJm" }, "outputs": [], "source": [ "client.close()\n", "trainer.save_model(output_dir)\n", "trainer.push_to_hub()" ] }, { "cell_type": "markdown", "metadata": { "id": "X-6lB52GAl_u" }, "source": [ "## Load the Fine-Tuned Model and Run Inference\n", "\n", "Now let's test our fine-tuned model by loading the **adapter** and running **inference**. \n", "We begin by loading the **base model**, attaching the adapter, and obtaining the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "d686d3933bef4ea3a9fb58193495e970", "4ce63e90903a4f60be6694de976e7127", "a1003f172b954e218fdb00539d79a7d1", "859e82d390204d5d8a763bd61b356ae4", "b54f50facca141639f3412b86cfc433d", "ec156a0cf90a42059f2335d4bae0628e", "fd9f8dcdf82d4e39a9c72d0e25464c28", "8696f66460244e55b9d67fd2fe9d6e51", "6acb03abb643408b8f802704f00674f5", "e13e54f756874f78af67a25564d64375", "5e29181a5ec1421d9f2eefbf7529d363", "fa420825fdab47d6aea18cd352dd9ef1", "9a460882ac834c8a90d77eec9a2c34ea", "7bf3166759f64fcf8968a6d282f8df85" ] }, "id": "-Vu--VueKyJm", "outputId": "399ccb1e-45bf-4305-ae9d-97edece48b53" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d686d3933bef4ea3a9fb58193495e970", "version_major": 2, "version_minor": 0 }, "text/plain": [ "config.json: 0.00B [00:00, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "4ce63e90903a4f60be6694de976e7127", "version_major": 2, "version_minor": 0 }, "text/plain": [ "model.safetensors.index.json: 0.00B [00:00, ?B/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a1003f172b954e218fdb00539d79a7d1", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Fetching 2 files: 0%| | 0/2 [00:00= 1.0:\n", " final_position_reward = 1.0\n", " else:\n", " final_position_reward = position_scores[-1] if position_scores else 0.0\n", "\n", " return {\n", " \"prompt_ids\": prompt_ids,\n", " \"completion_ids\": completion_ids,\n", " \"logprobs\": logprobs,\n", " \"env_mask\": env_mask,\n", " \"raw_rewards\": raw_rewards,\n", " \"correct_reward\": correct_reward_value,\n", " \"position_reward\": final_position_reward,\n", " \"model_outputs\": model_outputs,\n", " }" ] }, { "cell_type": "markdown", "id": "cipvIDzcoF3C", "metadata": { "id": "cipvIDzcoF3C" }, "source": [ "### Helper functions\n", "\n", "Supporting utilities used in `rollout_once`:\n", "\n", "- **`make_user_prompt`**: builds the user prompt combining the conversation history.\n", "- **`format_history`**: formats the conversation log for consistent context." ] }, { "cell_type": "code", "execution_count": null, "id": "bVeKfbaK7C4z", "metadata": { "id": "bVeKfbaK7C4z" }, "outputs": [], "source": [ "# @title Helpers definition (click to expand)\n", "def format_history(messages) -> str:\n", " lines = []\n", " for message in messages:\n", " tag = message.category or \"MESSAGE\"\n", " content = message.content.strip()\n", " if not content:\n", " continue\n", " lines.append(f\"[{tag}] {content}\")\n", " return \"\\n\".join(lines)\n", "\n", "\n", "def make_user_prompt(prompt_text, messages) -> str:\n", " history = format_history(messages)\n", " # Only use messages for conversation history - the prompt is already included as the first message\n", " history_section = history if history else \"[PROMPT] Awaiting first feedback.\"\n", " return f\"Conversation so far:\\n{history_section}\\n\\nReply with your next guess enclosed in square brackets.\"" ] }, { "cell_type": "markdown", "id": "i3G0x0RheYkL", "metadata": { "id": "i3G0x0RheYkL" }, "source": [ "## Define reward functions\n", "\n", "To guide the agent's learning process, we define simple reward functions that map the feedback from the environment into numeric signals. \n", "Each function corresponds to a specific aspect of the **Wordle** game:\n", "\n", "- ✅ **`reward_correct`**: rewards the model when it guesses the correct word (binary: 0 or 1). \n", "- 🎯 **`reward_position`**: rewards progress based on letter feedback. Green letters worth 1.0, yellow worth 0.5, normalized by 5. If the model wins, this is set to 1.0.\n", "- 📝 **`reward_format_strict`**: rewards correct output format `[xxxxx]`. Returns proportion of correctly formatted outputs across all turns.\n", "\n", "These functions return lists of float values that the **GRPOTrainer** uses during optimization. \n", "By combining them, the model learns to balance correctness, information gathering, and proper formatting in its guessing strategy." ] }, { "cell_type": "code", "execution_count": null, "id": "61e454d1-9abc-42a6-868c-a24e9801ac44", "metadata": { "id": "61e454d1-9abc-42a6-868c-a24e9801ac44" }, "outputs": [], "source": [ "def reward_correct(completions, **kwargs):\n", " \"\"\"Reward from environment (correct answer).\"\"\"\n", " rewards = kwargs.get(\"correct_reward\") if kwargs else None\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def reward_position(completions, **kwargs):\n", " \"\"\"Position reward: green worth 1.0, yellow worth 0.5, normalized by 5.\"\"\"\n", " rewards = kwargs.get(\"position_reward\") if kwargs else None\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]\n", "\n", "\n", "def compute_format_reward(model_outputs):\n", " \"\"\"Compute format reward from a list of model outputs (one per turn).\n", "\n", " Each output should be exactly [5 letters] with optional whitespace.\n", " Returns proportion of correctly formatted outputs.\n", " \"\"\"\n", " if not model_outputs:\n", " return 0.0\n", "\n", " exact_pattern = re.compile(r\"^\\s*\\[[A-Za-z]{5}\\]\\s*$\")\n", " correct_count = sum(1 for output in model_outputs if exact_pattern.match(output))\n", "\n", " return correct_count / len(model_outputs)\n", "\n", "\n", "def reward_format_strict(completions, **kwargs):\n", " \"\"\"Format reward - pre-computed in rollout_func.\"\"\"\n", " rewards = kwargs.get(\"format_reward\") if kwargs else None\n", " if rewards is None:\n", " return [0.0 for _ in completions]\n", " return [float(r) for r in rewards]" ] }, { "cell_type": "markdown", "id": "RN5VkehojyOJ", "metadata": { "id": "RN5VkehojyOJ" }, "source": [ "## Create dataset\n", "\n", "We create a dataset with repeated prompts to control the number of training episodes. \n", "Each entry in the dataset triggers one rollout episode during training. The `dataset_prompt` provides the initial instruction to the model before each game starts." ] }, { "cell_type": "code", "execution_count": null, "id": "deab8040-9b51-4c52-befe-e48578cdbb53", "metadata": { "id": "deab8040-9b51-4c52-befe-e48578cdbb53" }, "outputs": [], "source": [ "from datasets import Dataset\n", "\n", "dataset_size = 3000\n", "dataset_prompt = \"Play Wordle like an expert.\"\n", "\n", "dataset = Dataset.from_dict({\"prompt\": [dataset_prompt] * dataset_size})" ] }, { "cell_type": "markdown", "id": "DnR90-D66Fm_", "metadata": { "id": "DnR90-D66Fm_" }, "source": [ "## Set GRPO Config\n", "\n", "Next, we define the **GRPOConfig**, which controls all key training parameters. \n", "This configuration specifies how the model interacts with **vLLM**, manages memory, and logs results." ] }, { "cell_type": "code", "execution_count": null, "id": "20ac9371-af1a-4b9e-b678-33d6a3bf07cc", "metadata": { "id": "20ac9371-af1a-4b9e-b678-33d6a3bf07cc" }, "outputs": [], "source": [ "from trl import GRPOConfig\n", "\n", "output_dir = \"wordle-grpo-Qwen3-1.7B-test\"\n", "\n", "grpo_config = GRPOConfig(\n", " # Training schedule / optimization\n", " num_train_epochs = 1, # Number of full dataset passes\n", " learning_rate = 1e-6, # Learning rate for the optimizer\n", " gradient_accumulation_steps = 64, # Accumulate gradients over multiple steps\n", " per_device_train_batch_size = 1, # Batch size per GPU (number of prompts processed together)\n", " warmup_steps = 10, # Steps for learning rate warmup\n", " optim=\"adamw_torch\", # Optimizer\n", " max_grad_norm=1.0, # Clip gradients to prevent explosion\n", "\n", " # GRPO configuration\n", " num_generations = 2, # Number of rollout episodes per prompt (for variance reduction)\n", " max_completion_length=1024, # Full episode length, not per-turn\n", " log_completions = False, # Log completions for debugging\n", "\n", " # vLLM configuration\n", " use_vllm = True, # Enable vLLM for faster inference during rollouts\n", " vllm_mode = \"colocate\", # Run vLLM in colocate mode (same process as training)\n", " vllm_gpu_memory_utilization = 0.15, # Fraction of GPU memory reserved for vLLM inference\n", " vllm_max_model_length=3072, # Maximum context length for vLLM\n", " vllm_importance_sampling_mode=\"token_truncate\", # Less aggressive than default sequence_mask\n", "\n", " # Logging / reporting\n", " output_dir = output_dir, # Directory for checkpoints and logs\n", " report_to=\"trackio\", # Experiment tracking tool (integrates with HF Spaces)\n", " trackio_space_id = output_dir, # HF Space where experiment tracking will be saved\n", " logging_steps = 1, # Log metrics every N steps\n", " save_steps = 10, # Interval for saving checkpoints\n", " save_total_limit=1, # Max number of checkpoints to save\n", "\n", " # Memory optimization\n", " gradient_checkpointing = True, # Enable activation recomputation to save memory\n", "\n", " # Hub integration\n", " push_to_hub = True, # Set True to automatically push model to Hugging Face Hub\n", ")" ] }, { "cell_type": "markdown", "id": "Mrs9bAr06H2G", "metadata": { "id": "Mrs9bAr06H2G" }, "source": [ "## Create `GRPOTrainer` and start training\n", "\n", "Now we initialize the `GRPOTrainer`, which manages the entire reinforcement learning loop.\n", "\n", "It takes the model, tokenizer, reward functions, rollout function, and dataset defined earlier. \n", "The trainer coordinates the interaction between the model and the environment, applies the reward signals, and updates the policy.\n", "\n", "Finally, we call `trainer.train()` to start the fine-tuning process and let the model learn to play Wordle through feedback and iteration." ] }, { "cell_type": "code", "execution_count": null, "id": "FeBMCppH7rAc", "metadata": { "id": "FeBMCppH7rAc" }, "outputs": [], "source": [ "import sys\n", "sys.stdout.fileno = lambda: 1\n", "sys.stderr.fileno = lambda: 2" ] }, { "cell_type": "code", "execution_count": null, "id": "1f7aceb9-fe9e-49ba-b976-a39c1e29d4e5", "metadata": { "colab": { "referenced_widgets": [ "f44d7bb668064bdb80e3904ff92da5ea", "efa028ffbd704a489729c83af0647d68" ] }, "id": "1f7aceb9-fe9e-49ba-b976-a39c1e29d4e5", "outputId": "aa6f81a6-662c-4215-f091-bcf422f43f9c" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "f44d7bb668064bdb80e3904ff92da5ea", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading checkpoint shards: 0%| | 0/2 [00:00" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* GPU detected, enabling automatic GPU metrics logging\n", "* Created new run: sergiopaniego-1770031943\n" ] }, { "data": { "text/html": [ "\n", "

\n", " \n", " \n", " [93/93 3:18:36, Epoch 1/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
10.009800
20.016400
30.005600
40.014700
50.019500
60.002300
70.005300
80.025100
90.004500
100.004200
110.009600
120.014900
130.024500
140.012200
150.015500
160.007400
170.017500
180.014900
190.035600
200.014900
210.030000
220.014300
230.018000
240.014000
250.016600
260.015600
270.021300
280.021000
290.036900
300.006400
310.044800
320.026400
330.038700
340.022000
350.013400
360.025000
370.042900
380.072700
390.070100
400.019900
410.058700
420.060100
43-0.026700
440.038900
450.042400
46-0.009100
470.001300
480.020200
490.078700
500.026300
510.045700
520.035300
53-0.006700
540.025300
550.069500
560.092800
570.067900
580.035000
590.061300
600.048800
610.000600
620.028400
630.016200
640.010700
650.020200
660.041800
670.006800
680.014800
690.025100
70-0.006600
710.041000
720.008300
730.045300
740.062800
750.048200
760.032800
770.053000
780.023100
790.014900
800.078200
81-0.000700
820.013400
830.030200
84-0.003600
850.051700
860.033500
870.021800
88-0.003400
890.023200
90-0.002900
910.030900
920.029200
930.002500

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n", "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio (please wait...)\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/jupyter_client/session.py:203: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).\n", " return datetime.utcnow().replace(tzinfo=utc)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "id": "o-hEO4oK4ZXr", "metadata": { "id": "o-hEO4oK4ZXr" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "id": "zuHTwuxAVp8p", "metadata": { "id": "zuHTwuxAVp8p", "outputId": "fce9bdc8-d734-4382-bb26-7e03dbffa7a0" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "12065.8973 seconds used for training.\n", "201.1 minutes used for training.\n", "Peak reserved memory = 38.139 GB.\n", "Peak reserved memory for training = 25.655 GB.\n", "Peak reserved memory % of max memory = 96.415 %.\n", "Peak reserved memory for training % of max memory = 64.856 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_training = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "training_memory_percentage = round(used_memory_for_training / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_training} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {training_memory_percentage} %.\")" ] }, { "cell_type": "code", "execution_count": null, "id": "13e9fd4e-e7a5-468d-a25a-3f7d2794201f", "metadata": { "colab": { "referenced_widgets": [ "decd9f00c4da42bf92b72c327bd28278", "2d924050f7bf4e7f88316c8fc202a763", "d589783221084eb7833ae6cd742d277c", "0e135c821b5744b287b4de7eeb15d419", "a1839712ff344a409e6f7f48a1467fd5", "e9ae0fcd43e34d7e916fe1bda0a38a49", "75776d6523ef42df930ddfd7048b384e", "e2e07a449d914bd39653b7cbbc5903e3", "0eafc3f9bac14807866233f924793380", "b64c487a9dff4108a66da9eee4e4ed66", "17a3ba38cf7349269ea54df84faf30b7", "7382295b99ee4db28de43e1451dd0d17" ] }, "id": "13e9fd4e-e7a5-468d-a25a-3f7d2794201f", "outputId": "7f703ed8-7874-4da1-8490-48222755ae11" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "decd9f00c4da42bf92b72c327bd28278", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "2d924050f7bf4e7f88316c8fc202a763", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d589783221084eb7833ae6cd742d277c", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...7B-test/training_args.bin: 100%|##########| 7.70kB / 7.70kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "0e135c821b5744b287b4de7eeb15d419", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...-1.7B-test/tokenizer.json: 100%|##########| 11.4MB / 11.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a1839712ff344a409e6f7f48a1467fd5", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0002-of-00002.safetensors: 2%|1 | 33.5MB / 1.91GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e9ae0fcd43e34d7e916fe1bda0a38a49", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0001-of-00002.safetensors: 1%| | 33.5MB / 4.97GB " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "No files have been modified since last commit. Skipping to prevent empty commit.\n", "WARNING:huggingface_hub.hf_api:No files have been modified since last commit. Skipping to prevent empty commit.\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "75776d6523ef42df930ddfd7048b384e", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "e2e07a449d914bd39653b7cbbc5903e3", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "0eafc3f9bac14807866233f924793380", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...7B-test/training_args.bin: 100%|##########| 7.70kB / 7.70kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b64c487a9dff4108a66da9eee4e4ed66", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...-1.7B-test/tokenizer.json: 100%|##########| 11.4MB / 11.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "17a3ba38cf7349269ea54df84faf30b7", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0001-of-00002.safetensors: 1%| | 33.5MB / 4.97GB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7382295b99ee4db28de43e1451dd0d17", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...0002-of-00002.safetensors: 2%|1 | 33.5MB / 1.91GB " ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stderr", "output_type": "stream", "text": [ "No files have been modified since last commit. Skipping to prevent empty commit.\n", "WARNING:huggingface_hub.hf_api:No files have been modified since last commit. Skipping to prevent empty commit.\n" ] }, { "data": { "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" }, "text/plain": [ "CommitInfo(commit_url='https://huggingface.co/sergiopaniego/wordle-grpo-Qwen3-1.7B-test/commit/2d7a27066ef244796a079cbf08fa6656af426145', commit_message='End of training', commit_description='', oid='2d7a27066ef244796a079cbf08fa6656af426145', pr_url=None, repo_url=RepoUrl('https://huggingface.co/sergiopaniego/wordle-grpo-Qwen3-1.7B-test', endpoint='https://huggingface.co', repo_type='model', repo_id='sergiopaniego/wordle-grpo-Qwen3-1.7B-test'), pr_revision=None, pr_num=None)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "env.close()\n", "trainer.save_model(output_dir)\n", "trainer.push_to_hub()" ] }, { "cell_type": "markdown", "id": "wQyVb1nAxWld", "metadata": { "id": "wQyVb1nAxWld" }, "source": [ "## Load the Fine-Tuned Model and Run Inference\n", "\n", "Now let's test our fine-tuned model by loading the **adapter** and running **inference**. \n", "We begin by loading the **base model**, attaching the adapter, and obtaining the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "id": "JcTeeSBXxWWF", "metadata": { "colab": { "referenced_widgets": [ "281b1cf074fd4d60bb754906a0764865", "e129fb465f1a41c1bdf2495d14143458" ] }, "id": "JcTeeSBXxWWF", "outputId": "86efafc3-1161-471b-86b1-14c43e95908f" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:104: UserWarning: \n", "Error while fetching `HF_TOKEN` secret value from your vault: 'Requesting secret HF_TOKEN timed out. Secrets can only be fetched when running from the Colab UI.'.\n", "You are not authenticated with the Hugging Face Hub in this notebook.\n", "If the error persists, please let us know by opening an issue on GitHub (https://github.com/huggingface/huggingface_hub/issues/new).\n", " warnings.warn(\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "281b1cf074fd4d60bb754906a0764865", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Fetching 2 files: 0%| | 0/2 [00:00 {generated_text}\")\n", " print(f\" Parsed guess: {guess}\")\n", "\n", " result = env.step(TextArenaAction(message=guess))\n", " observation = result.observation\n", "\n", " print(\" Feedback messages:\")\n", " for message in observation.messages:\n", " print(f\" [{message.category}] {message.content}\")\n", "\n", " print(\"\\n✅ Game finished\")\n", " print(f\" Reward: {result.reward}\")\n", " print(f\" Done: {result.done}\")" ] }, { "cell_type": "markdown", "id": "MjIxHOHK4PVe", "metadata": { "id": "MjIxHOHK4PVe" }, "source": [ "Let's play the game!" ] }, { "cell_type": "code", "execution_count": null, "id": "JjOzWexUXmfW", "metadata": { "id": "JjOzWexUXmfW", "outputId": "1c6130af-fe89-4930-e53a-7329e0483ef0" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📜 Initial Prompt:\n", "You are Player 0 in Wordle.\n", "A secret 5-letter word has been chosen. You have 6 attempts to guess it.\n", "For each guess, wrap your word in square brackets (e.g., [apple]).\n", "Feedback for each letter will be given as follows:\n", " - G (green): correct letter in the correct position\n", " - Y (yellow): letter exists in the word but in the wrong position\n", " - X (wrong): letter is not in the word\n", "Enter your guess to begin.\n", "\n", "🎯 Turn 0: model replied with -> [crane]\n", " Parsed guess: [crane]\n", " Feedback messages:\n", " [MESSAGE] [crane]\n", " [MESSAGE] Player 0 submitted [crane].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "You have 5 guesses left.\n", "\n", "🎯 Turn 1: model replied with -> [spare]\n", " Parsed guess: [spare]\n", " Feedback messages:\n", " [MESSAGE] [spare]\n", " [MESSAGE] Player 0 submitted [spare].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "You have 4 guesses left.\n", "\n", "🎯 Turn 2: model replied with -> [spare]\n", " Parsed guess: [spare]\n", " Feedback messages:\n", " [MESSAGE] [spare]\n", " [MESSAGE] Player 0 submitted [spare].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "You have 3 guesses left.\n", "\n", "🎯 Turn 3: model replied with -> [spare]\n", " Parsed guess: [spare]\n", " Feedback messages:\n", " [MESSAGE] [spare]\n", " [MESSAGE] Player 0 submitted [spare].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "You have 2 guesses left.\n", "\n", "🎯 Turn 4: model replied with -> [spare]\n", " Parsed guess: [spare]\n", " Feedback messages:\n", " [MESSAGE] [spare]\n", " [MESSAGE] Player 0 submitted [spare].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "You have 1 guesses left.\n", "\n", "🎯 Turn 5: model replied with -> [spare]\n", " Parsed guess: [spare]\n", " Feedback messages:\n", " [MESSAGE] [spare]\n", " [MESSAGE] Player 0 submitted [spare].\n", "Feedback:\n", "C R A N E\n", "X Y X X X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "S P A R E\n", "G X X G X\n", "\n", "You have 0 guesses left.\n", " [MESSAGE] The game ended in a draw. Reason: Turn limit reached.\n", "\n", "✅ Game finished\n", " Reward: 0.0\n", " Done: True\n" ] } ], "source": [ "try:\n", " play_wordle(env, fine_tuned_model, tokenizer)\n", "finally:\n", " env.close()" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "A100", "provenance": [] }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: examples/notebooks/sft_ministral3_vl.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "UaDIwQOOjgAO" }, "source": [ "# Supervised Fine-Tuning (SFT) Ministral-3 with QLoRA using TRL\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_ministral3_vl.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "4f0hzSo4kKEc" }, "source": [ "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can fine-tune cutting edge vision language models. It comes with support for quantized parameter efficient fine-tuning technique **QLoRA**, so we can use free Colab (T4 GPU) to fine-tune models like [Ministral-3](https://huggingface.co/collections/mistralai/ministral-3).\n", "\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples (notebooks and scripts)](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)" ] }, { "cell_type": "markdown", "metadata": { "id": "pGXgIbj2kXEP" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, and **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "V8rqrGq3hmeU", "outputId": "4a15adc2-e895-4c40-d174-c52e0b208dd5" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" bitsandbytes trackio git+https://github.com/huggingface/transformers mistral-common" ] }, { "cell_type": "markdown", "metadata": { "id": "Ou0VO1gHklS-" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "C5eHAVFthmeU" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "vNylrNdqkoN-" }, "source": [ "## Load dataset\n", "\n", "\n", "We'll load the [**trl-lib/llava-instruct-mix**](https://huggingface.co/datasets/trl-lib/llava-instruct-mix) dataset from the Hugging Face Hub using the `datasets` library.\n", "\n", "This dataset is a set of GPT-generated multimodal instruction-following data. We use a processed version for conveniency here. You can check out more details about how to configure your own multimodal dataset for traininig with SFT in the [docs](https://huggingface.co/docs/trl/en/sft_trainer#training-vision-language-models). Fine-tuning Ministral-3 on it helps refine its response style and visual understanding.\n", "\n", "\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "e0bb4423267a4572b1b9cc894edd25c5" ] }, "id": "hOPra_x5hmeU", "outputId": "112a213e-0036-452f-e0a8-9295da13c3d1" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = \"trl-lib/llava-instruct-mix\"\n", "train_dataset = load_dataset(dataset_name, split=\"train[:10%]\")" ] }, { "cell_type": "markdown", "metadata": { "id": "JFtR4Xyx4FYO" }, "source": [ "Let's review one example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "vYJGczm6hmeV", "outputId": "0a9d8771-51bd-4b68-c1b0-d8b96da9b56d" }, "outputs": [ { "data": { "text/plain": [ "{'images': [],\n", " 'prompt': [{'content': \"How can the presentation of this meal influence one's eating experience?\",\n", " 'role': 'user'}],\n", " 'completion': [{'content': \"The presentation of this meal can positively influence one's eating experience. In the image, colorful plastic trays and bowls are used to hold a variety of foods, including meat, vegetables, fruit, and bread. The vibrant presentation can make the meal more visually appealing and enticing, which may encourage healthier eating habits as the dishes include nutritious options like broccoli and oranges. Diverse food options and attractive meal presentation can also make the dining experience more enjoyable and satisfying. Moreover, the bright colors and well-organized food placement can create a positive atmosphere, enhancing one's overall dining experience.\",\n", " 'role': 'assistant'}]}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_dataset[0]" ] }, { "cell_type": "markdown", "metadata": { "id": "qeZCtRB1m5xj" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration.\n", "\n", "> **Note:**\n", "> In older GPUs (including those available on Colab), **FP8 support** is limited, so we use the BF16 version of the model.\n", "> In that case, you can select the official checkpoint or the one from Unsloth.\n", "> If you have access to GPUs with **FP8 support**, you can switch to that version instead." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "5fa6df349d314dd8b1baf79c1ed4eb0b", "f561335214d84bf1b8985dd4573e9ae1", "0c557dfead7d46e99af2c6e77b050930" ] }, "id": "8dggHeG2hmeV", "outputId": "58ceedc1-26b3-467d-f466-e76d850aca5f" }, "outputs": [], "source": [ "from transformers import Mistral3ForConditionalGeneration, FineGrainedFP8Config, BitsAndBytesConfig\n", "import torch\n", "\n", "FP8 = False\n", "\n", "if FP8:\n", " model_name = \"mistralai/Ministral-3-3B-Instruct-2512\"\n", " quantization_config = FineGrainedFP8Config(dequantize=False)\n", "else:\n", " model_name = \"mistralai/Ministral-3-3B-Instruct-2512-BF16\" # \"unsloth/Ministral-3-3B-Instruct-2512\"\n", " quantization_config = BitsAndBytesConfig(\n", " load_in_4bit=True, # Load the model in 4-bit precision to save memory\n", " bnb_4bit_compute_dtype=torch.float16, # Data type used for internal computations in quantization\n", " bnb_4bit_use_double_quant=True, # Use double quantization to improve accuracy\n", " bnb_4bit_quant_type=\"nf4\", # Type of quantization. \"nf4\" is recommended for recent LLMs\n", " )\n", "\n", "model = Mistral3ForConditionalGeneration.from_pretrained(\n", " model_name,\n", " dtype=\"float32\",\n", " device_map=\"auto\",\n", " quantization_config=quantization_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "jyklRvNxnHmy" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter** — a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8wI1Cqk4hmeV" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different VLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=32,\n", " lora_alpha=32,\n", " target_modules=['down_proj','o_proj','k_proj','q_proj','gate_proj','up_proj','v_proj'],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "mBAfaiA-nbdm" }, "source": [ "## Train model\n", "\n", "We'll configure **SFT** using `SFTConfig`, keeping the parameters minimal so the training fits on a free Colab instance. You can adjust these settings if more resources are available. For full details on all available parameters, check the [TRL SFTConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.SFTConfig)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "FrbfENGThmeV" }, "outputs": [], "source": [ "from trl import SFTConfig\n", "\n", "output_dir = \"Ministral-3-3B-Instruct-trl-sft\"\n", "\n", "training_args = SFTConfig(\n", " # Training schedule / optimization\n", " #num_train_epochs=1,\n", " max_steps=10, # Number of dataset passes. For full trainings, use `num_train_epochs` instead\n", " per_device_train_batch_size=2, # Batch size per GPU/CPU\n", " gradient_accumulation_steps=8, # Gradients are accumulated over multiple steps → effective batch size = 4 * 8 = 32\n", " warmup_steps=5, # Gradually increase LR during first N steps\n", " learning_rate=2e-4, # Learning rate for the optimizer\n", " optim=\"adamw_8bit\", # Optimizer\n", " max_length=None, # For VLMs, truncating may remove image tokens, leading to errors during training. max_length=None avoids it\n", "\n", " # Logging / reporting\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=1, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id = output_dir,\n", "\n", " # Hub integration\n", " push_to_hub=True,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "bF4GtNO2ne1k" }, "source": [ "Configure the SFT Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "JjLhVbO_hmeV" }, "outputs": [], "source": [ "from trl import SFTTrainer\n", "\n", "trainer = SFTTrainer(\n", " model=model,\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "K9Ub3jTDnfcD" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "p3UUrqCWhmeV", "outputId": "992da74b-7e6b-41f0-cb71-320203c1d6d9" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GPU = Tesla T4. Max memory = 14.741 GB.\n", "6.346 GB of memory reserved.\n" ] } ], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "4NiFu9tcniBP" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MA8py8DghmeV", "outputId": "b68f35e1-cfdd-413f-b3d7-5afa4d30aac5" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "* Trackio project initialized: huggingface\n", "* Trackio metrics will be synced to Hugging Face Dataset: sergiopaniego/Ministral-3-3B-Instruct-trl-sft-dataset\n", "* Creating new space: https://huggingface.co/spaces/sergiopaniego/Ministral-3-3B-Instruct-trl-sft\n", "* View dashboard by going to: https://sergiopaniego-Ministral-3-3B-Instruct-trl-sft.hf.space/\n" ] }, { "data": { "text/html": [ "

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Created new run: sergiopaniego-1764766746\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/torch/_dynamo/eval_frame.py:1044: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. Starting in PyTorch 2.9, calling checkpoint without use_reentrant will raise an exception. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.\n", " return fn(*args, **kwargs)\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [10/10 44:39, Epoch 0/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
11.979992
21.894323
31.924157
41.396819
51.357613
61.345677
71.356363
81.399492
91.356316
101.307108

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "miZ2I1A9nnM4" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "eUi4ww17hmeV", "outputId": "24b18fc3-cb0f-40a1-954d-c3b4d799dfbb" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2492.1064 seconds used for training.\n", "41.54 minutes used for training.\n", "Peak reserved memory = 13.881 GB.\n", "Peak reserved memory for training = 7.535 GB.\n", "Peak reserved memory % of max memory = 94.166 %.\n", "Peak reserved memory for training % of max memory = 51.116 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "3lrrYfPunloQ" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "ee7d9fe2f71343a4a4832fd2b0fef66f", "7afb48c0ac514171aa052210e9063fe7", "7047f7b527284d958df2ec5e800cce56", "8a106d43be4948ffbb2a2a5281882fe0", "1f3deb9e16554fd0a4666d52fa9992b2", "4cb20a75e85a495a8a3b36eb513db36b", "7ded518429454b5aa2230f98ba0d014c", "7112497495a742a887c4c956cdf46777", "cae8cfc256f649d5aa367f923b4aeb5f", "1a2cfcd18f5f496fa4f6bcf5c0a33966" ] }, "id": "S7TzHDwXhmeV", "outputId": "0d465e97-5459-4f59-84cf-d5c374503a3b" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "pFq51FWEK1DX" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Xh4fo-WzhmeV" }, "outputs": [], "source": [ "output_dir = \"Ministral-3-3B-Instruct-trl-sft\"\n", "# model_name = \"mistralai/Ministral-3-3B-Instruct-2512\"\n", "model_name = \"mistralai/Ministral-3-3B-Instruct-2512-BF16\" # \"unsloth/Ministral-3-3B-Instruct-2512\"" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "5b4514b95d7742a5a2cc777478a152b7", "fdfc2ee13efa40218ab315641eb62fb7", "9aad01ac11794c9ca74d1a442e715da5", "384a08933c11424cbc48dc28f33ec90e" ] }, "id": "z9S319H-hmeV", "outputId": "6b5ef891-cc0d-44ba-b112-3c577247a295" }, "outputs": [], "source": [ "from transformers import Mistral3ForConditionalGeneration, MistralCommonBackend\n", "from peft import PeftModel\n", "\n", "base_model = model_name\n", "adapter_model = f\"{output_dir}\" # Replace with your HF username or organization + fine-tuned model name\n", "\n", "model = Mistral3ForConditionalGeneration.from_pretrained(base_model, dtype=\"float32\", device_map=\"auto\")\n", "model = PeftModel.from_pretrained(model, adapter_model)\n", "\n", "tokenizer = MistralCommonBackend.from_pretrained(base_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "EvObNndEhmeW" }, "outputs": [], "source": [ "import base64\n", "from io import BytesIO\n", "\n", "problem = train_dataset[0]['prompt'][0]['content']\n", "image = train_dataset[0]['images'][0]\n", "\n", "buffer = BytesIO()\n", "image.save(buffer, format=\"JPEG\")\n", "image_bytes = buffer.getvalue()\n", "image_b64 = base64.b64encode(image_bytes).decode(\"utf-8\")\n", "\n", "messages = [\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\n", " \"type\": \"image_url\",\n", " \"image_url\": {\n", " \"url\": f\"data:image/jpeg;base64,{image_b64}\"\n", " },\n", " },\n", " {\"type\": \"text\", \"text\": problem},\n", " ],\n", " },\n", "]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "yjBaVAevhmeW", "outputId": "47695454-856a-40b9-8ab2-d621c6e6b3da" }, "outputs": [], "source": [ "messages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "CD2BVqCBhmeW", "outputId": "fe1e612b-209f-4223-eaf3-9432deb0d467" }, "outputs": [ { "data": { "image/jpeg": "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", "image/png": "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", "text/plain": [ "" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "image" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "YU3hwodphmeW", "outputId": "7e1757f6-0c77-477a-c35a-2429d8020374" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The presentation of the meal, which is divided into three compartments in a lunch box, can significantly influence one's eating experience. The compartmentalized design allows for a visually appealing and organized presentation of the food, making it more enticing and appetizing. The variety of food items, such as the sandwich, chips, and fruit, also adds to the visual appeal and encourages a balanced and nutritious meal. The compartmentalized layout can also make it easier for the person eating the meal to access and enjoy each component of the meal, as they can easily reach for the food they want without having to rearrange the entire meal. Overall, the presentation of the meal can enhance the eating experience by making it more enjoyable, visually appealing, and convenient.\n" ] } ], "source": [ "import torch\n", "\n", "tokenized = tokenizer.apply_chat_template(messages, return_tensors=\"pt\", return_dict=True)\n", "tokenized[\"input_ids\"] = tokenized[\"input_ids\"].to(device=\"cuda\")\n", "tokenized[\"pixel_values\"] = tokenized[\"pixel_values\"].to(dtype=torch.bfloat16, device=\"cuda\")\n", "image_sizes = [tokenized[\"pixel_values\"].shape[-2:]]\n", "\n", "output = model.generate(\n", " **tokenized,\n", " image_sizes=image_sizes,\n", " max_new_tokens=512,\n", ")[0]\n", "\n", "decoded_output = tokenizer.decode(output[len(tokenized[\"input_ids\"][0]):])\n", "print(decoded_output)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/sft_nemotron_3.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "id": "ovlmqboji0c", "metadata": { "id": "ovlmqboji0c" }, "source": "# Fine-Tune NVIDIA Nemotron 3 with SFT and LoRA using TRL\n\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_nemotron_3.ipynb)" }, { "cell_type": "markdown", "id": "kiezlcdkw2k", "metadata": { "id": "kiezlcdkw2k" }, "source": [ "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)" ] }, { "cell_type": "markdown", "id": "ezgoxs28857", "metadata": { "id": "ezgoxs28857" }, "source": "Fine-tune [**NVIDIA Nemotron 3**](https://huggingface.co/collections/nvidia/nvidia-nemotron-v3) models using **LoRA** and **SFT** with the [**TRL**](https://github.com/huggingface/trl) library.\n\nThe Nemotron 3 family includes hybrid **Mamba2/Transformer** models in different sizes (Nano, Super, etc.), natively supported in `transformers` (no `trust_remote_code` needed). See the [full collection](https://huggingface.co/collections/nvidia/nvidia-nemotron-v3) for all available checkpoints.\n\n> **Note:** This notebook requires a GPU with sufficient VRAM (e.g., A100 80GB). It is not designed for free Colab instances.\n\n- [TRL GitHub Repository](https://github.com/huggingface/trl)\n- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview)" }, { "cell_type": "markdown", "id": "5t9o6vg3jus", "metadata": { "id": "5t9o6vg3jus" }, "source": "## Install dependencies\n\nWe install **TRL** with the **PEFT** and **quantization** extras, along with **trackio** for experiment tracking. Nemotron 3 models use **Mamba2** layers, which require the `mamba_ssm` and `causal_conv1d` CUDA kernels." }, { "cell_type": "code", "execution_count": null, "id": "wh29y02x1ni", "metadata": { "id": "wh29y02x1ni" }, "outputs": [], "source": "!pip install -Uq \"trl[peft,quantization]\" trackio\n\n# Nemotron 3 requires transformers>=5.3.0, which may not be installed by default with TRL\n!pip install -Uq \"transformers>=5.3.0\"\n\n# Mamba2 CUDA kernels (--no-build-isolation needed so they find your PyTorch/CUDA installation)\n!pip install --no-build-isolation mamba_ssm==2.2.5 # installation takes around 30 mins\n!pip install --no-build-isolation causal_conv1d==1.5.2" }, { "cell_type": "markdown", "id": "yic7cr26xoi", "metadata": { "id": "yic7cr26xoi" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model and track experiments on the Hub. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "id": "wwocbqwne49", "metadata": { "id": "wwocbqwne49", "outputId": "f8a3f993-63a5-4a11-ea41-5a0093cec19c", "colab": { "referenced_widgets": [ "20976278f8564bc080426e851d52f46f" ] } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/huggingface_hub/utils/_auth.py:86: UserWarning: \n", "Access to the secret `HF_TOKEN` has not been granted on this notebook.\n", "You will not be requested again.\n", "Please restart the session if you want to be prompted again.\n", " warnings.warn(\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "20976278f8564bc080426e851d52f46f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(HTML(value='

...` tags. We also remove the extra columns that are not needed for training:" ] }, { "cell_type": "code", "execution_count": null, "id": "oxzelzs2kf", "metadata": { "id": "oxzelzs2kf" }, "outputs": [], "source": [ "def merge_thinking_and_remove_key(example):\n", " new_messages = []\n", " for msg in example[\"messages\"]:\n", " content = msg[\"content\"]\n", " thinking = msg.get(\"thinking\")\n", " if thinking and isinstance(thinking, str) and thinking.strip():\n", " content = f\"\\n{thinking}\\n\\n{content}\"\n", " new_messages.append({\"role\": msg[\"role\"], \"content\": content})\n", " example[\"messages\"] = new_messages\n", " return example\n", "\n", "train_dataset = train_dataset.remove_columns([\"reasoning_language\", \"developer\", \"user\", \"analysis\", \"final\"])\n", "train_dataset = train_dataset.map(merge_thinking_and_remove_key)" ] }, { "cell_type": "markdown", "id": "gyh82o2b7i7", "metadata": { "id": "gyh82o2b7i7" }, "source": "## Load model and configure LoRA\n\nLoad an **NVIDIA Nemotron 3** model. These models are natively supported in `transformers` so no `trust_remote_code` is needed. We use `attn_implementation=\"eager\"` as required by the hybrid Mamba2/Transformer architecture." }, { "cell_type": "code", "execution_count": null, "id": "asrptlebp6a", "metadata": { "id": "asrptlebp6a" }, "outputs": [], "source": "import torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\n# Select a Nemotron 3 checkpoint below\nmodel_id = \"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16\"\n# model_id = \"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-Base-BF16\"\n\noutput_dir = \"nemotron-3-sft\"\n\nmodel = AutoModelForCausalLM.from_pretrained(\n model_id,\n attn_implementation=\"eager\",\n dtype=torch.bfloat16,\n)\ntokenizer = AutoTokenizer.from_pretrained(model_id)" }, { "cell_type": "markdown", "id": "90a8pzer8rc", "metadata": { "id": "90a8pzer8rc" }, "source": [ "Configure the **LoRA adapter**. Instead of modifying the original weights, we fine-tune a lightweight LoRA adapter for efficient and memory-friendly training." ] }, { "cell_type": "code", "execution_count": null, "id": "pyukji9o3g", "metadata": { "id": "pyukji9o3g" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "peft_config = LoraConfig(\n", " r=8,\n", " lora_alpha=16,\n", " target_modules=[\n", " \"q_proj\",\n", " \"k_proj\",\n", " \"v_proj\",\n", " \"o_proj\",\n", " \"gate_proj\",\n", " \"up_proj\",\n", " \"down_proj\",\n", " ],\n", ")" ] }, { "cell_type": "markdown", "id": "rkxovj1wywh", "metadata": { "id": "rkxovj1wywh" }, "source": "## Train model\n\nConfigure **SFT** using `SFTConfig`. Note that `gradient_checkpointing` is set to `False` because Nemotron 3 (`NemotronHForCausalLM`) does not support it. For full details on all available parameters, check the [TRL SFTConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.SFTConfig)." }, { "cell_type": "code", "execution_count": null, "id": "tdmoy72oow", "metadata": { "id": "tdmoy72oow" }, "outputs": [], "source": [ "from trl import SFTConfig\n", "\n", "training_args = SFTConfig(\n", " # Training schedule / optimization\n", " per_device_train_batch_size=1, # Batch size per GPU\n", " gradient_accumulation_steps=4, # Effective batch size = per_device_train_batch_size * gradient_accumulation_steps\n", " num_train_epochs=1, # Number of full dataset passes\n", " learning_rate=2e-4, # Learning rate for the optimizer\n", " optim=\"paged_adamw_8bit\", # Memory-efficient 8-bit optimizer\n", "\n", " # Logging / reporting\n", " logging_steps=10, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id=output_dir, # HF Space where the experiment tracking will be saved\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", "\n", " max_length=128, # Kept short due to VRAM constraints; increase for better results\n", " gradient_checkpointing=False, # NemotronH does not support gradient checkpointing\n", "\n", " # Hub integration\n", " push_to_hub=True, # Push the trained model to the Hugging Face Hub\n", ")" ] }, { "cell_type": "markdown", "id": "4ciesqgt2ch", "metadata": { "id": "4ciesqgt2ch" }, "source": [ "Configure the SFT Trainer. We pass the previously configured `training_args` and the `peft_config` for LoRA." ] }, { "cell_type": "code", "execution_count": null, "id": "h0i5c9sd1ip", "metadata": { "id": "h0i5c9sd1ip" }, "outputs": [], "source": [ "from trl import SFTTrainer\n", "\n", "trainer = SFTTrainer(\n", " model=model,\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "id": "q1t7v0lba6l", "metadata": { "id": "q1t7v0lba6l" }, "source": [ "Show memory stats before training:" ] }, { "cell_type": "code", "execution_count": null, "id": "b4e0fseivvu", "metadata": { "id": "b4e0fseivvu", "outputId": "94e1ffbb-639c-4dba-d2b7-2d1de0f493bf" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GPU = NVIDIA A100-SXM4-80GB. Max memory = 79.251 GB.\n", "58.939 GB of memory reserved.\n" ] } ], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "id": "1x3fdnogsvh", "metadata": { "id": "1x3fdnogsvh" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "id": "3ev9j7op2mi", "metadata": { "id": "3ev9j7op2mi", "outputId": "d6db6cdb-8d43-4cc6-e024-e77c266090f7" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'eos_token_id': 11, 'pad_token_id': None}.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "* Trackio project initialized: huggingface\n", "* Trackio metrics will be synced to Hugging Face Dataset: sergiopaniego/nemotron-3-sft-dataset\n", "* Creating new space: https://huggingface.co/spaces/sergiopaniego/nemotron-3-sft\n", "* View dashboard by going to: https://sergiopaniego-nemotron-3-sft.hf.space/\n" ] }, { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* NVIDIA GPU detected, enabling automatic GPU metrics logging\n", "* Created new run: sergiopaniego-1773149018\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/usr/local/lib/python3.12/dist-packages/transformers/models/nemotron_h/modeling_nemotron_h.py:1215: FutureWarning: `input_embeds` is deprecated and will be removed in version 5.6.0 for `create_causal_mask`. Use `inputs_embeds` instead.\n", " causal_mask = create_causal_mask(\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [250/250 08:53, Epoch 1/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
102.379619
201.849189
301.466606
401.207237
501.064882
601.067329
701.085463
800.964451
900.993209
1001.061509
1100.976885
1200.939225
1301.011990
1400.989627
1500.946731
1600.939989
1700.920945
1801.003975
1900.945233
2000.958358
2101.022524
2200.953711
2301.033258
2400.955309
2500.959105

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio Space (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "id": "ngrmymxpbp", "metadata": { "id": "ngrmymxpbp" }, "source": [ "Show memory stats after training:" ] }, { "cell_type": "code", "execution_count": null, "id": "08gpt482fn1k", "metadata": { "id": "08gpt482fn1k", "outputId": "e4808857-5756-4f5d-d34c-29c81107a618" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "542.949 seconds used for training.\n", "9.05 minutes used for training.\n", "Peak reserved memory = 61.922 GB.\n", "Peak reserved memory for training = 2.983 GB.\n", "Peak reserved memory % of max memory = 78.134 %.\n", "Peak reserved memory for training % of max memory = 3.764 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "id": "pkp4wfpwpxh", "metadata": { "id": "pkp4wfpwpxh" }, "source": [ "## Save fine-tuned model\n", "\n", "Save the fine-tuned model both **locally** and to the **Hugging Face Hub**." ] }, { "cell_type": "code", "execution_count": null, "id": "g2j7qp1ycxl", "metadata": { "id": "g2j7qp1ycxl", "outputId": "5791fc37-589b-4bb9-ca66-85434b303787", "colab": { "referenced_widgets": [ "20def6e82e1d4b69ae1b677a39a54bfc", "53fe5574751540b68f2d6ef047cccc3c", "3661c026040f44f3870921e8a2db7bd2", "61b4f98716b649dc88332367852ec89c", "a61820f54e5445ceaf27efa956212271", "d761f8d2a0ff4de2a8728a1998e5f919", "3fea49f4e519416f81af219023d45c34", "240c5e45b5f245f984cdaaea9ff3a380", "7ba75fdca7da467cacc407da59382376", "2b3e22abd6704ef6b5cb2455c6fa98ab" ] } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "20def6e82e1d4b69ae1b677a39a54bfc", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "53fe5574751540b68f2d6ef047cccc3c", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3661c026040f44f3870921e8a2db7bd2", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...n-3-sft/training_args.bin: 100%|##########| 5.58kB / 5.58kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "61b4f98716b649dc88332367852ec89c", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...adapter_model.safetensors: 100%|##########| 13.2MB / 13.2MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a61820f54e5445ceaf27efa956212271", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...tron-3-sft/tokenizer.json: 100%|##########| 17.1MB / 17.1MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d761f8d2a0ff4de2a8728a1998e5f919", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "3fea49f4e519416f81af219023d45c34", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "240c5e45b5f245f984cdaaea9ff3a380", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...n-3-sft/training_args.bin: 100%|##########| 5.58kB / 5.58kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7ba75fdca7da467cacc407da59382376", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...tron-3-sft/tokenizer.json: 100%|##########| 17.1MB / 17.1MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "2b3e22abd6704ef6b5cb2455c6fa98ab", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...adapter_model.safetensors: 100%|##########| 13.2MB / 13.2MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" }, "text/plain": [ "CommitInfo(commit_url='https://huggingface.co/sergiopaniego/nemotron-3-sft/commit/dd05a083358e10113019b06da76c3e9ab4c4c3e4', commit_message='End of training', commit_description='', oid='dd05a083358e10113019b06da76c3e9ab4c4c3e4', pr_url=None, repo_url=RepoUrl('https://huggingface.co/sergiopaniego/nemotron-3-sft', endpoint='https://huggingface.co', repo_type='model', repo_id='sergiopaniego/nemotron-3-sft'), pr_revision=None, pr_num=None)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "id": "jp3u13oqlye", "metadata": { "id": "jp3u13oqlye" }, "source": [ "## Inference\n", "\n", "Let's run the fine-tuned model using standard `transformers` generation (`model.generate`)." ] }, { "cell_type": "code", "execution_count": null, "id": "1tb9jai824gi", "metadata": { "id": "1tb9jai824gi" }, "outputs": [], "source": [ "model = trainer.model\n", "model.eval()" ] }, { "cell_type": "code", "execution_count": null, "id": "3zp0i0zw4je", "metadata": { "id": "3zp0i0zw4je" }, "outputs": [], "source": [ "messages = [{\"role\": \"user\", \"content\": \"Continue the sequence: 1, 1, 2, 3, 5, 8,\"}]\n", "text = tokenizer.apply_chat_template(\n", " messages,\n", " tokenize=False,\n", " add_generation_prompt=True,\n", ")\n", "\n", "from transformers import TextStreamer\n", "\n", "_ = model.generate(\n", " **tokenizer(text, return_tensors=\"pt\").to(\"cuda\"),\n", " max_new_tokens=128,\n", " temperature=0.7,\n", " top_p=0.8,\n", " top_k=20,\n", " streamer=TextStreamer(tokenizer, skip_prompt=True),\n", ")" ] } ], "metadata": { "language_info": { "name": "python" }, "colab": { "provenance": [], "machine_shape": "hm", "gpuType": "A100" }, "accelerator": "GPU" }, "nbformat": 4, "nbformat_minor": 5 } ================================================ FILE: examples/notebooks/sft_qwen_vl.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "UaDIwQOOjgAO" }, "source": [ "# Supervised Fine-Tuning (SFT) Qwen3-VL with QLoRA using TRL\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_qwen_vl.ipynb)\n", "\n", "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "4f0hzSo4kKEc" }, "source": [ "With [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl), you can fine-tune cutting edge vision language models. It comes with support for quantized parameter efficient fine-tuning technique **QLoRA**, so we can use free Colab (T4 GPU) to fine-tune models like [Qwen3-VL](https://huggingface.co/collections/Qwen/qwen3-vl-68d2a7c1b8a8afce4ebd2dbe).\n", "\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)\n", "- [More Qwen3-VL Fine-tuning Examples (including TRL scripts)](https://github.com/QwenLM/Qwen3-VL/tree/main/qwen-vl-finetune/)" ] }, { "cell_type": "markdown", "metadata": { "id": "pGXgIbj2kXEP" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, and **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8CfZlUevmkg7" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" bitsandbytes trackio" ] }, { "cell_type": "markdown", "metadata": { "id": "Ou0VO1gHklS-" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "4Ncx0wYtnYCW" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "vNylrNdqkoN-" }, "source": [ "## Load dataset\n", "\n", "\n", "We'll load the [**trl-lib/llava-instruct-mix**](https://huggingface.co/datasets/trl-lib/llava-instruct-mix) dataset from the Hugging Face Hub using the `datasets` library.\n", "\n", "This dataset is a set of GPT-generated multimodal instruction-following data. We use a processed version for conveniency here. You can check out more details about how to configure your own multimodal dataset for traininig with SFT in the [docs](https://huggingface.co/docs/trl/en/sft_trainer#training-vision-language-models). Fine-tuning Qwen3-VL on it helps refine its response style and visual understanding.\n", "\n", "\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0AcyX6Jd1_hp" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = \"trl-lib/llava-instruct-mix\"\n", "train_dataset = load_dataset(dataset_name, split=\"train[:10%]\")" ] }, { "cell_type": "markdown", "metadata": { "id": "JFtR4Xyx4FYO" }, "source": [ "Let's review one example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "YLrEY_v8m0eA" }, "outputs": [], "source": [ "train_dataset[0]" ] }, { "cell_type": "markdown", "metadata": { "id": "qeZCtRB1m5xj" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gt05dgXgm9QR" }, "outputs": [], "source": [ "from transformers import Qwen3VLForConditionalGeneration, BitsAndBytesConfig\n", "import torch\n", "\n", "model_name = \"Qwen/Qwen3-VL-4B-Instruct\" # \"Qwen/Qwen3-VL-8B-Instruct\"\n", "\n", "model = Qwen3VLForConditionalGeneration.from_pretrained(\n", " model_name,\n", " dtype=\"float32\",\n", " device_map=\"auto\",\n", " quantization_config=BitsAndBytesConfig(\n", " load_in_4bit=True, # Load the model in 4-bit precision to save memory\n", " bnb_4bit_compute_dtype=torch.float16, # Data type used for internal computations in quantization\n", " bnb_4bit_use_double_quant=True, # Use double quantization to improve accuracy\n", " bnb_4bit_quant_type=\"nf4\" # Type of quantization. \"nf4\" is recommended for recent LLMs\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "jyklRvNxnHmy" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter** — a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ME1im5gh2LFg" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different VLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=32,\n", " lora_alpha=32,\n", " target_modules=['down_proj','o_proj','k_proj','q_proj','gate_proj','up_proj','v_proj'],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "mBAfaiA-nbdm" }, "source": [ "## Train model\n", "\n", "We'll configure **SFT** using `SFTConfig`, keeping the parameters minimal so the training fits on a free Colab instance. You can adjust these settings if more resources are available. For full details on all available parameters, check the [TRL SFTConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.SFTConfig)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "GQPxXvu-2Ngc" }, "outputs": [], "source": [ "from trl import SFTConfig\n", "\n", "output_dir = \"Qwen3-VL-4B-Instruct-trl-sft\"\n", "\n", "# Configure training arguments using SFTConfig\n", "training_args = SFTConfig(\n", " # Training schedule / optimization\n", " #num_train_epochs=1,\n", " max_steps=10, # Number of dataset passes. For full trainings, use `num_train_epochs` instead\n", " per_device_train_batch_size=2, # Batch size per GPU/CPU\n", " gradient_accumulation_steps=8, # Gradients are accumulated over multiple steps → effective batch size = 4 * 8 = 32\n", " warmup_steps=5, # Gradually increase LR during first N steps\n", " learning_rate=2e-4, # Learning rate for the optimizer\n", " optim=\"adamw_8bit\", # Optimizer\n", " max_length=None, # For VLMs, truncating may remove image tokens, leading to errors during training. max_length=None avoids it\n", "\n", " # Logging / reporting\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", " logging_steps=1, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", "\n", " # Hub integration\n", " push_to_hub=True,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "bF4GtNO2ne1k" }, "source": [ "Configure the SFT Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "TwBeQKQC2RfZ" }, "outputs": [], "source": [ "from trl import SFTTrainer\n", "\n", "trainer = SFTTrainer(\n", " model=model,\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config,\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "K9Ub3jTDnfcD" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "u6_Vsv_1KtVU" }, "outputs": [], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "4NiFu9tcniBP" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "pbJXrhA0ywra" }, "outputs": [], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "miZ2I1A9nnM4" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8jegvQGlKyEu" }, "outputs": [], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "3lrrYfPunloQ" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MNfRlfIGKSHI" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "pFq51FWEK1DX" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "wAm1iQc8K1uY" }, "outputs": [], "source": [ "from transformers import Qwen3VLForConditionalGeneration, AutoProcessor\n", "from peft import PeftModel\n", "\n", "base_model = model_name\n", "adapter_model = f\"{output_dir}\" # Replace with your HF username or organization + fine-tuned model name\n", "\n", "model = Qwen3VLForConditionalGeneration.from_pretrained(base_model, dtype=\"float32\", device_map=\"auto\")\n", "model = PeftModel.from_pretrained(model, adapter_model)\n", "\n", "processor = AutoProcessor.from_pretrained(base_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "a5rLWJdOvwGQ" }, "outputs": [], "source": [ "problem = train_dataset[0]['prompt'][0]['content']\n", "image = train_dataset[0]['images'][0]\n", "\n", "messages = [\n", " {\n", " \"role\": \"user\",\n", " \"content\": [\n", " {\"type\": \"image\", \"image\": image},\n", " {\"type\": \"text\", \"text\": problem},\n", " ],\n", " },\n", "]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qiu-ROFeBPhA" }, "outputs": [], "source": [ "messages" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 497 }, "id": "qGzEXSH5BQwG", "outputId": "611d6bfd-fb72-4737-e847-1611132d49ed" }, "outputs": [ { "data": { "image/jpeg": "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", "image/png": "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", "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "image" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "SG64xtCtv24-", "outputId": "cdc347b4-cdf2-49c8-e308-71dc99f27476" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[\"The presentation of this meal can influence one's eating experience by making it more visually appealing and appetizing. The vibrant colors of the fruits, vegetables, and bread, as well as the careful arrangement of the food in the colorful containers, can create a sense of variety and abundance. This can make the meal more enjoyable and satisfying to eat, encouraging individuals to eat more and feel more satisfied after eating. Additionally, the presentation can make the meal more inviting and appealing to others, increasing the likelihood that they will eat it.\"]\n" ] } ], "source": [ "inputs = processor.apply_chat_template(\n", " messages,\n", " add_generation_prompt=True,\n", " tokenize=True,\n", " return_tensors=\"pt\"\n", " return_dict=True,\n", ").to(model.device)\n", "\n", "# Inference: Generation of the output\n", "generated_ids = model.generate(**inputs, max_new_tokens=128)\n", "generated_ids_trimmed = [\n", " out_ids[len(in_ids) :] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)\n", "]\n", "output_text = processor.batch_decode(\n", " generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False\n", ")\n", "print(output_text)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/sft_tool_calling.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "ii5Zkit6eSqU" }, "source": [ "# Teaching Tool Calling with Supervised Fine-Tuning (SFT) using TRL on a Free Colab Notebook\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_tool_calling.ipynb)" ] }, { "cell_type": "markdown", "metadata": { "id": "gJVcVKOteSqV" }, "source": [ "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "hzt0BrvoeSqW" }, "source": [ "Learn how to teach a language model to perform **tool calling** using **Supervised Fine-Tuning (SFT)** with **LoRA/QLoRA** and the [**TRL**](https://github.com/huggingface/trl) library.\n", "\n", "The model used in this notebook does not have native tool-calling support. We extend its Jinja2 chat template (via `tiny_aya_chat_template.jinja`) to serialize tool schemas into the system preamble and render tool calls as structured `` XML inside the model's native `<|START_RESPONSE|>` / `<|END_RESPONSE|>` delimiters. The modified template is saved with the tokenizer, making inference reproducible: just load the tokenizer from the output directory and call `apply_chat_template` with `tools=TOOLS`.\n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project!\n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview)\n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)" ] }, { "cell_type": "markdown", "metadata": { "id": "3PfX1aj5eSqW" }, "source": [ "## Key concepts\n", "\n", "- **SFT**: Trains a model on example input-output pairs to align its behavior with a desired task.\n", "- **Tool Calling**: The ability of a model to respond with a structured function call instead of free-form text.\n", "- **LoRA**: Updates only a small set of low-rank parameters, reducing training cost and memory usage.\n", "- **QLoRA**: A quantized variant of LoRA that enables fine-tuning larger models on limited hardware.\n", "- **TRL**: The Hugging Face library that makes fine-tuning and reinforcement learning simple and efficient." ] }, { "cell_type": "markdown", "metadata": { "id": "QDMcKeoEeSqW" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which brings in all main dependencies such as **Transformers** and **PEFT** (parameter-efficient fine-tuning). We also install **trackio** for experiment logging, and **bitsandbytes** for 4-bit quantization," ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Ey-TuYPrXTLG", "outputId": "a4fd8cfe-624e-4185-ab59-e6901514cb96" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.0/1.0 MB\u001b[0m \u001b[31m17.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m60.7/60.7 MB\u001b[0m \u001b[31m42.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m24.2/24.2 MB\u001b[0m \u001b[31m109.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m56.0/56.0 kB\u001b[0m \u001b[31m6.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m9.9/9.9 MB\u001b[0m \u001b[31m131.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m540.5/540.5 kB\u001b[0m \u001b[31m44.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", "\u001b[?25h" ] } ], "source": [ "!pip install -Uq \"trl[peft]\" trackio bitsandbytes liger-kernel" ] }, { "cell_type": "markdown", "metadata": { "id": "Aw8_T-Z0eSqW" }, "source": [ "### Log in to Hugging Face\n", "\n", "Log in to your Hugging Face account to push the fine-tuned model to the Hub and access gated models. You can find your access token on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "_qaeDZwXXTLG" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "XPnDpJgIeSqX" }, "source": [ "## Load Dataset\n", "\n", "We load the [**bebechien/SimpleToolCalling**](https://huggingface.co/datasets/bebechien/SimpleToolCalling) dataset, which contains user queries paired with the correct tool call to handle each request. Each sample provides a `user_content`, a `tool_name`, and `tool_arguments`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zfJY_8AzXTLG" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = \"bebechien/SimpleToolCalling\"\n", "dataset = load_dataset(dataset_name, split=\"train\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ygeMXzKGXTLH", "outputId": "a1ed3a8b-f515-4cda-eeb2-db0355ed2c02" }, "outputs": [ { "data": { "text/plain": [ "Dataset({\n", " features: ['user_content', 'tool_name', 'tool_arguments'],\n", " num_rows: 40\n", "})" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dataset" ] }, { "cell_type": "markdown", "metadata": { "id": "O_GkvqtReSqX" }, "source": [ "## Prepare Tool-Calling Data\n", "\n", "We define two tools: `search_knowledge_base` for internal company documents and `search_google` for public information. We then write a custom Jinja2 chat template that extends the model's default template with two additions:\n", "\n", "1. A **Tool Use** section is appended to the system preamble when `tools` is passed to `apply_chat_template`.\n", "2. Assistant turns with `tool_calls` render the call as structured `` inside the model's existing `<|START_RESPONSE|>` / `<|END_RESPONSE|>` delimiters.\n", "\n", "Each training sample uses the standard `tool_calls` message format with a `tools` key — SFTTrainer passes these to `apply_chat_template` automatically." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jaAgXeWtXTLH" }, "outputs": [], "source": [ "import json\n", "\n", "# These are the tool schemas that are used in the dataset\n", "TOOLS = [\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"search_knowledge_base\",\n", " \"description\": \"Search internal company documents, policies and project data.\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"query string\"}},\n", " \"required\": [\"query\"],\n", " },\n", " \"return\": {\"type\": \"string\"},\n", " },\n", " },\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": \"search_google\",\n", " \"description\": \"Search public information.\",\n", " \"parameters\": {\n", " \"type\": \"object\",\n", " \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"query string\"}},\n", " \"required\": [\"query\"],\n", " },\n", " \"return\": {\"type\": \"string\"},\n", " },\n", " },\n", "]\n", "\n", "def create_conversation(sample):\n", " return {\n", " \"prompt\": [{\"role\": \"user\", \"content\": sample[\"user_content\"]}],\n", " \"completion\": [\n", " {\n", " \"role\": \"assistant\",\n", " \"tool_calls\": [\n", " {\n", " \"type\": \"function\",\n", " \"function\": {\n", " \"name\": sample[\"tool_name\"],\n", " \"arguments\": json.loads(sample[\"tool_arguments\"]),\n", " },\n", " }\n", " ],\n", " },\n", " ],\n", " \"tools\": TOOLS,\n", " }" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "32p512R2XTLH" }, "outputs": [], "source": [ "dataset = dataset.map(create_conversation, remove_columns=dataset.features)\n", "\n", "# Split dataset into 50% training samples and 50% test samples\n", "dataset = dataset.train_test_split(test_size=0.5, shuffle=True)" ] }, { "cell_type": "markdown", "metadata": { "id": "Plnjef-PeSqX" }, "source": [ "Let's inspect an example from the training set to verify the format:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "f4QI6wJjXTLH", "outputId": "2156adb4-7bed-4e29-84c5-54e6d45e5500" }, "outputs": [ { "data": { "text/plain": [ "{'messages': [{'content': 'How do I configure the VPN for the New York office?',\n", " 'role': 'user',\n", " 'tool_calls': None},\n", " {'content': None,\n", " 'role': 'assistant',\n", " 'tool_calls': [{'function': {'arguments': {'query': 'VPN configuration guide New York office'},\n", " 'name': 'search_knowledge_base'},\n", " 'type': 'function'}]}],\n", " 'tools': [{'function': {'description': 'Search internal company documents, policies and project data.',\n", " 'name': 'search_knowledge_base',\n", " 'parameters': {'properties': {'query': {'description': 'query string',\n", " 'type': 'string'}},\n", " 'required': ['query'],\n", " 'type': 'object'},\n", " 'return': {'type': 'string'}},\n", " 'type': 'function'},\n", " {'function': {'description': 'Search public information.',\n", " 'name': 'search_google',\n", " 'parameters': {'properties': {'query': {'description': 'query string',\n", " 'type': 'string'}},\n", " 'required': ['query'],\n", " 'type': 'object'},\n", " 'return': {'type': 'string'}},\n", " 'type': 'function'}]}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dataset['train'][0]" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "fBIGKl_UXTLH", "outputId": "edd8e968-c7e4-418d-b9e9-26773aee1366" }, "outputs": [ { "data": { "text/plain": [ "DatasetDict({\n", " train: Dataset({\n", " features: ['messages', 'tools'],\n", " num_rows: 20\n", " })\n", " test: Dataset({\n", " features: ['messages', 'tools'],\n", " num_rows: 20\n", " })\n", "})" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dataset" ] }, { "cell_type": "markdown", "metadata": { "id": "aud6U3c2eSqX" }, "source": [ "## Load Model and Configure LoRA/QLoRA\n", "\n", "Choose the model you want to fine-tune. This notebook uses [`CohereLabs/tiny-aya-global`](https://huggingface.co/CohereLabs/tiny-aya-global) by default." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "_j_LF12IXTLH" }, "outputs": [], "source": [ "model_id, output_dir = \"CohereLabs/tiny-aya-global\", \"tiny-aya-global-SFT\" # ✅ ~9.1 GB VRAM" ] }, { "cell_type": "markdown", "metadata": { "id": "gpTZHjpJeSqX" }, "source": [ "Load the model with 4-bit quantization using `BitsAndBytesConfig` (QLoRA). To use standard LoRA without quantization, comment out the `quantization_config` parameter. We also load the tokenizer separately so we can install the custom chat template before training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "680888237b78477ea653adb2ecea7fa8" ] }, "id": "jGpTDV6sXTLH", "outputId": "fc33f7a6-bfd0-4228-80cd-e0aeb67bbd42" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "680888237b78477ea653adb2ecea7fa8", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading weights: 0%| | 0/290 [00:00" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* GPU detected, enabling automatic GPU metrics logging\n", "* Created new run: sergiopaniego-1771428231\n" ] }, { "data": { "text/html": [ "\n", "

\n", " \n", " \n", " [15/15 00:52, Epoch 3/3]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
13.095131
23.083373
32.951535
42.625918
52.254464
61.939976
71.694891
81.558982
91.430660
101.305176
111.192725
121.120383
131.052859
140.985858
150.970833

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "4MGKFi1-eSqY" }, "source": [ "Show memory stats after training:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "3f68GA6TXTLI", "outputId": "321e90ee-757a-41fc-c6a2-4ba40a6e6b3c" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "59.2841 seconds used for training.\n", "0.99 minutes used for training.\n", "Peak reserved memory = 11.928 GB.\n", "Peak reserved memory for training = 7.28 GB.\n", "Peak reserved memory % of max memory = 30.202 %.\n", "Peak reserved memory for training % of max memory = 18.433 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "ONWy4NOAeSqY" }, "source": [ "## Save the Fine-Tuned Model\n", "\n", "Save the trained LoRA adapter locally and push it to the Hugging Face Hub." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "4951424bb90e4dbbaea8c9b88c592872", "1669afd0e52443d090adab0fbe663c66", "5ee5f6b74e7246eea99c0d84c2a27bc0", "15b4a13592c14102af0d3f8a999f3d36", "7216a7d56c364a0d92e079d9848946d3", "83e8f73e00004b718cbba7be0ecc45e1", "05db21352f614288864f88c1ba794ee9", "d7821b8cd21f4fb78237479ff081511b", "00c9f462a4584b22b1e38dfcc5f86af3", "22cacea841ba48c29b7a74ea17a50b4e" ] }, "id": "9qz-fRZyXTLI", "outputId": "9ff41250-0786-4ec6-fe41-dfc6b611d0b5" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "4951424bb90e4dbbaea8c9b88c592872", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "1669afd0e52443d090adab0fbe663c66", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "5ee5f6b74e7246eea99c0d84c2a27bc0", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...bal-SFT/training_args.bin: 100%|##########| 5.58kB / 5.58kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "15b4a13592c14102af0d3f8a999f3d36", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...global-SFT/tokenizer.json: 100%|##########| 21.4MB / 21.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "7216a7d56c364a0d92e079d9848946d3", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...adapter_model.safetensors: 35%|###4 | 41.9MB / 121MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "83e8f73e00004b718cbba7be0ecc45e1", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Processing Files (0 / 0) : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "05db21352f614288864f88c1ba794ee9", "version_major": 2, "version_minor": 0 }, "text/plain": [ "New Data Upload : | | 0.00B / 0.00B " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "d7821b8cd21f4fb78237479ff081511b", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...bal-SFT/training_args.bin: 100%|##########| 5.58kB / 5.58kB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "00c9f462a4584b22b1e38dfcc5f86af3", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...adapter_model.safetensors: 35%|###4 | 41.9MB / 121MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "22cacea841ba48c29b7a74ea17a50b4e", "version_major": 2, "version_minor": 0 }, "text/plain": [ " ...global-SFT/tokenizer.json: 100%|##########| 21.4MB / 21.4MB " ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.google.colaboratory.intrinsic+json": { "type": "string" }, "text/plain": [ "CommitInfo(commit_url='https://huggingface.co/sergiopaniego/tiny-aya-global-SFT/commit/c59baa62c6bb5a3c3be2d33b482522a00783a5b4', commit_message='End of training', commit_description='', oid='c59baa62c6bb5a3c3be2d33b482522a00783a5b4', pr_url=None, repo_url=RepoUrl('https://huggingface.co/sergiopaniego/tiny-aya-global-SFT', endpoint='https://huggingface.co', repo_type='model', repo_id='sergiopaniego/tiny-aya-global-SFT'), pr_revision=None, pr_num=None)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "wNA4AIE4SiUg" }, "source": [ "## Load the Fine-Tuned Model and Run Inference\n", "\n", "Load the trained LoRA adapter on top of the base model and merge it into the weights for efficient inference." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "referenced_widgets": [ "9d6a109e605d440ab2c115d969796859" ] }, "id": "b5CmxYtpXTLI", "outputId": "10ebe012-9ffe-4096-f155-648af855aa80" }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "9d6a109e605d440ab2c115d969796859", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading weights: 0%| | 0/290 [00:00\n", "\n", "node.js latest version\n", "\n", "\n", "\n" ] } ], "source": [ "sample_test_data = dataset[\"test\"][0] # Get a sample from the test set\n", "\n", "user_content = sample_test_data[\"prompt\"]\n", "\n", "print(f\"User Query: {user_content}\")\n", "\n", "predicted_output = generate_prediction(user_content)\n", "print(f\"Predicted Output: {predicted_output}\")" ] }, { "cell_type": "markdown", "metadata": { "id": "-r85c-aa7C7k" }, "source": [ "You can still use the strong multilingual model capabilities:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "UGePqQGVXTLI", "outputId": "adcd21ca-ca45-43d5-a3cc-02a47377e51b" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "User Query: [{'role': 'user', 'content': \"Explica en español qué significa la palabra japonesa 'ikigai' y da un ejemplo práctico.\"}]\n", "Predicted Output: \n", "\n", "ikigai significado y ejemplo\n", "\n", "\n", "\n" ] } ], "source": [ "user_content = \"Explica en español qué significa la palabra japonesa 'ikigai' y da un ejemplo práctico.\" # Spanish question\n", "user_content = [{\"role\": \"user\", \"content\": user_content}]\n", "\n", "print(f\"User Query: {user_content}\")\n", "\n", "predicted_output = generate_prediction(user_content)\n", "print(f\"Predicted Output: {predicted_output}\")" ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "provenance": [] }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/notebooks/sft_trl_lora_qlora.ipynb ================================================ { "cells": [ { "cell_type": "markdown", "metadata": { "id": "5oqSnSaqLWAL" }, "source": [ "# Supervised Fine-Tuning (SFT) with LoRA/QLoRA using TRL — on a Free Colab Notebook\n", "\n", "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/huggingface/trl/blob/main/examples/notebooks/sft_trl_lora_qlora.ipynb)" ] }, { "cell_type": "markdown", "metadata": { "id": "d6c1x17tLWAR" }, "source": [ "![trl banner](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/trl_banner_dark.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "cQ6bxQaMLWAS" }, "source": [ "Easily fine-tune Large Language Models (LLMs) or Vision-Language Models (VLMs) with **LoRA** or **QLoRA** using the [**Transformers Reinforcement Learning (TRL)**](https://github.com/huggingface/trl) library built by Hugging Face — all within a **free Google Colab notebook** (powered by a **T4 GPU**.). \n", "\n", "- [TRL GitHub Repository](https://github.com/huggingface/trl) — star us to support the project! \n", "- [Official TRL Examples](https://huggingface.co/docs/trl/example_overview) \n", "- [Community Tutorials](https://huggingface.co/docs/trl/community_tutorials)" ] }, { "cell_type": "markdown", "metadata": { "id": "JG3wax0uLWAU" }, "source": [ "## Key concepts\n", "\n", "- **SFT**: Trains models from example input-output pairs to align behavior with human preferences.\n", "- **LoRA**: Updates only a few low-rank parameters, reducing training cost and memory.\n", "- **QLoRA**: A quantized version of LoRA that enables even larger models to fit on small GPUs.\n", "- **TRL**: The Hugging Face library that makes fine-tuning and reinforcement learning simple and efficient.\n", "\n", "Learn how to perform **Supervised Fine-Tuning (SFT)** with **LoRA/QLoRA** using **TRL**." ] }, { "cell_type": "markdown", "metadata": { "id": "0ZhyNnhiLWAV" }, "source": [ "## Install dependencies\n", "\n", "We'll install **TRL** with the **PEFT** extra, which ensures all main dependencies such as **Transformers** and **PEFT** (a package for parameter-efficient fine-tuning, e.g., LoRA/QLoRA) are included. Additionally, we'll install **trackio** to log and monitor our experiments, and **bitsandbytes** to enable quantization of LLMs, reducing memory consumption for both inference and training." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "FXTyVTJcLWAV" }, "outputs": [], "source": [ "!pip install -Uq \"trl[peft]\" trackio bitsandbytes liger-kernel" ] }, { "cell_type": "markdown", "metadata": { "id": "OqlMF6oWLWAY" }, "source": [ "### Log in to Hugging Face" ] }, { "cell_type": "markdown", "metadata": { "id": "2blL6-1_LWAa" }, "source": [ "Log in to your **Hugging Face** account to save your fine-tuned model, track your experiment results directly on the Hub or access gated models. You can find your **access token** on your [account settings page](https://huggingface.co/settings/tokens)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "6OMeJOp7LWAc" }, "outputs": [], "source": [ "from huggingface_hub import notebook_login\n", "\n", "notebook_login()" ] }, { "cell_type": "markdown", "metadata": { "id": "6HHscLIQLWAd" }, "source": [ "## Load Dataset\n", "\n", "In this step, we load the [**HuggingFaceH4/Multilingual-Thinking**](https://huggingface.co/datasets/HuggingFaceH4/Multilingual-Thinking) dataset from the Hugging Face Hub using the `datasets` library. \n", "This dataset focuses on **multilingual reasoning**, where the *chain of thought* has been translated into several languages such as French, Spanish, and German. \n", "By fine-tuning a reasoning-capable model on this dataset, it learns to **generate reasoning steps in multiple languages**, making its thought process more **interpretable and accessible** to non-English speakers.\n", "\n", "> 💡 This dataset is best suited for models that already demonstrate reasoning capabilities. \n", "> If you're using a model without reasoning skills, consider choosing a different dataset. Example: [`trl-lib/llava-instruct-mix`](https://huggingface.co/datasets/trl-lib/llava-instruct-mix).\n", "\n", "For efficiency, we'll load only the **training split**:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "dlQSKxTnLWAd" }, "outputs": [], "source": [ "from datasets import load_dataset\n", "\n", "dataset_name = \"HuggingFaceH4/Multilingual-Thinking\"\n", "train_dataset = load_dataset(dataset_name, split=\"train\")" ] }, { "cell_type": "markdown", "metadata": { "id": "bRHTwwZXLWAe" }, "source": [ "This dataset contains different columns. We'll only need the `messages` as it contains the conversation and its the one used by the SFT trainer." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zOBq8tVdLWAe", "outputId": "e12ab8ae-e00c-4e89-b489-dd448db8e13b" }, "outputs": [ { "data": { "text/plain": [ "Dataset({\n", " features: ['reasoning_language', 'developer', 'user', 'analysis', 'final', 'messages'],\n", " num_rows: 1000\n", "})" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_dataset" ] }, { "cell_type": "markdown", "metadata": { "id": "b13TjFs2LWAe" }, "source": [ "Let's see a full example to understand the internal structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ZON5mIMNLWAf", "outputId": "d01415eb-26cb-45ce-ad48-0388161eea28" }, "outputs": [ { "data": { "text/plain": [ "{'reasoning_language': 'French',\n", " 'developer': 'You are an AI chatbot with a lively and energetic personality.',\n", " 'user': 'Can you show me the latest trends on Twitter right now?',\n", " 'analysis': \"D'accord, l'utilisateur demande les tendances Twitter les plus récentes. Tout d'abord, je dois vérifier si j'ai accès à des données en temps réel. Étant donné que je ne peux pas naviguer sur Internet ou accéder directement à l'API de Twitter, je ne peux pas fournir des tendances en direct. Cependant, je peux donner quelques conseils généraux sur la façon de les trouver.\\n\\nJe devrais préciser que les tendances Twitter évoluent rapidement et sont spécifiques à chaque région. Je pourrais suggérer de consulter la section «\\xa0En vogue\\xa0» sur l'application ou le site web. Aussi, l'utilisation de hashtags et le suivi d'utilisateurs pertinents pourraient être utiles. Il est important de souligner que les tendances varient selon la région et l'heure de la journée. Je devrais garder un ton amical et bienveillant, peut-être ajouter un emoji pour rester léger. Je vais structurer ma réponse étape par étape pour faciliter la lecture. Je dois m'excuser de ne pas pouvoir fournir des données en temps réel et proposer d'autres méthodes. Je conserverai un langage simple et convivial, en évitant les termes techniques.\",\n", " 'final': 'Hey there! While I can\\'t check Twitter (X) in real-time or access live data, I can share some tips to help you spot the latest trends:\\n\\n1. **Open the \"Trending\" tab** on the Twitter app or website – it updates constantly! \\n2. **Search for hashtags** like #Trending or #Viral to see what’s blowing up. \\n3. **Follow accounts** that curate trends (e.g., @TrendingNow, @ViralThreads). \\n4. **Check regional trends** – they often differ by location! \\n\\nRemember, trends are *super fast-moving* and often tied to pop culture, memes, or breaking news. For example, recent trends have included viral challenges (like the \"Distracted Boyfriend\" meme revival), celebrity drama, or unexpected events (hello, weather disasters!). \\n\\nWant me to brainstorm *what* might trend next? I’ve got ideas!',\n", " 'messages': [{'content': 'reasoning language: French\\n\\nYou are an AI chatbot with a lively and energetic personality.',\n", " 'role': 'system',\n", " 'thinking': None},\n", " {'content': 'Can you show me the latest trends on Twitter right now?',\n", " 'role': 'user',\n", " 'thinking': None},\n", " {'content': 'Hey there! While I can\\'t check Twitter (X) in real-time or access live data, I can share some tips to help you spot the latest trends:\\n\\n1. **Open the \"Trending\" tab** on the Twitter app or website – it updates constantly! \\n2. **Search for hashtags** like #Trending or #Viral to see what’s blowing up. \\n3. **Follow accounts** that curate trends (e.g., @TrendingNow, @ViralThreads). \\n4. **Check regional trends** – they often differ by location! \\n\\nRemember, trends are *super fast-moving* and often tied to pop culture, memes, or breaking news. For example, recent trends have included viral challenges (like the \"Distracted Boyfriend\" meme revival), celebrity drama, or unexpected events (hello, weather disasters!). \\n\\nWant me to brainstorm *what* might trend next? I’ve got ideas!',\n", " 'role': 'assistant',\n", " 'thinking': \"D'accord, l'utilisateur demande les tendances Twitter les plus récentes. Tout d'abord, je dois vérifier si j'ai accès à des données en temps réel. Étant donné que je ne peux pas naviguer sur Internet ou accéder directement à l'API de Twitter, je ne peux pas fournir des tendances en direct. Cependant, je peux donner quelques conseils généraux sur la façon de les trouver.\\n\\nJe devrais préciser que les tendances Twitter évoluent rapidement et sont spécifiques à chaque région. Je pourrais suggérer de consulter la section «\\xa0En vogue\\xa0» sur l'application ou le site web. Aussi, l'utilisation de hashtags et le suivi d'utilisateurs pertinents pourraient être utiles. Il est important de souligner que les tendances varient selon la région et l'heure de la journée. Je devrais garder un ton amical et bienveillant, peut-être ajouter un emoji pour rester léger. Je vais structurer ma réponse étape par étape pour faciliter la lecture. Je dois m'excuser de ne pas pouvoir fournir des données en temps réel et proposer d'autres méthodes. Je conserverai un langage simple et convivial, en évitant les termes techniques.\"}]}" ] }, "execution_count": null, "metadata": {}, "output_type": "execute_result" } ], "source": [ "train_dataset[0]" ] }, { "cell_type": "markdown", "metadata": { "id": "RPQfGZjlLWAf" }, "source": [ "\n", "Now, let's remove the columns that are not needed, as we just discussed:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "pCM6PoIzLWAf" }, "outputs": [], "source": [ "train_dataset = train_dataset.remove_columns(column_names=['reasoning_language', 'developer', 'user', 'analysis', 'final'])" ] }, { "cell_type": "markdown", "metadata": { "id": "BcU6E8KnLWAf" }, "source": [ "The `messages` column is specifically formatted according to the [Harmony response format](https://cookbook.openai.com/articles/openai-harmony) used by *gpt-oss*. \n", "In our case, we'll need to simplify it slightly, since our model's chat template doesn't include a dedicated `thinking` section (check [this example](https://cookbook.openai.com/articles/gpt-oss/fine-tune-transfomers) for more details). \n", "To adapt it, we'll merge that part into the message content using the standard `...` tags.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "XQ2xYEq3LWAf" }, "outputs": [], "source": [ "def merge_thinking_and_remove_key(example):\n", " new_messages = []\n", " for msg in example[\"messages\"]:\n", " content = msg[\"content\"]\n", " thinking = msg.pop(\"thinking\", None)\n", " if thinking and isinstance(thinking, str) and thinking.strip():\n", " content = f\"\\n{thinking}\\n\\n{content}\"\n", " msg[\"content\"] = content\n", " new_messages.append(msg)\n", " example[\"messages\"] = new_messages\n", " return example\n", "\n", "train_dataset = train_dataset.map(merge_thinking_and_remove_key)" ] }, { "cell_type": "markdown", "metadata": { "id": "ewvZeKUcLWAf" }, "source": [ "## Load model and configure LoRA/QLoRA\n", "\n", "This notebook can be used with two fine-tuning methods. By default, it is set up for **QLoRA**, which includes quantization using `BitsAndBytesConfig`. If you prefer to use standard **LoRA** without quantization, simply comment out the `BitsAndBytesConfig` configuration.\n", "\n", "Below, choose your **preferred model**. All of the options have been tested on **free Colab instances**." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "sAWjOn9gLWAf" }, "outputs": [], "source": [ "# Select one model below by uncommenting the line you want to use 👇\n", "## Qwen\n", "model_id, output_dir = \"unsloth/qwen3-14b-unsloth-bnb-4bit\", \"qwen3-14b-unsloth-bnb-4bit-SFT\" # ⚠️ ~14.1 GB VRAM\n", "# model_id, output_dir = \"Qwen/Qwen3-8B\", \"Qwen3-8B-SFT\" # ⚠️ ~12.8 GB VRAM\n", "# model_id, output_dir = \"Qwen/Qwen2.5-7B-Instruct\", \"Qwen2.5-7B-Instruct\" # ✅ ~10.8 GB VRAM\n", "\n", "## Llama\n", "# model_id, output_dir = \"meta-llama/Llama-3.2-3B-Instruct\", \"Llama-3.2-3B-Instruct\" # ✅ ~4.7 GB VRAM\n", "# model_id, output_dir = \"meta-llama/Llama-3.1-8B-Instruct\", \"Llama-3.1-8B-Instruct\" # ⚠️ ~10.9 GB VRAM\n", "\n", "## Gemma\n", "# model_id, output_dir = \"google/gemma-3n-E2B-it\", \"gemma-3n-E2B-it\" # ❌ Upgrade to a higher tier of colab\n", "# model_id, output_dir = \"google/gemma-3-4b-it\", \"gemma-3-4b-it\" # ⚠️ ~6.8 GB VRAM\n", "\n", "## Granite\n", "#model_id, output_dir = \"ibm-granite/granite-4.0-micro\", \"granite-4.0-micro\" # ✅ ~3.3 GB VRAM\n", "\n", "## LFM2\n", "#model_id, output_dir = \"LiquidAI/LFM2-2.6B\", \"LFM2-2.6B-SFT\" # ✅ ~5.89 GB VRAM" ] }, { "cell_type": "markdown", "metadata": { "id": "BXY9Y0_dLWAf" }, "source": [ "Let's load the selected model using `transformers`, configuring QLoRA via `bitsandbytes` (you can remove it if doing LoRA). We don't need to configure the tokenizer since the trainer takes care of that automatically." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "oyOoWFsLLWAg" }, "outputs": [], "source": [ "import torch\n", "from transformers import AutoModelForCausalLM, BitsAndBytesConfig\n", "\n", "model = AutoModelForCausalLM.from_pretrained(\n", " model_id,\n", " attn_implementation=\"sdpa\", # Change to Flash Attention if GPU has support\n", " dtype=torch.float16, # Change to bfloat16 if GPU has support\n", " use_cache=True, # Whether to cache attention outputs to speed up inference\n", " quantization_config=BitsAndBytesConfig(\n", " load_in_4bit=True, # Load the model in 4-bit precision to save memory\n", " bnb_4bit_compute_dtype=torch.float16, # Data type used for internal computations in quantization\n", " bnb_4bit_use_double_quant=True, # Use double quantization to improve accuracy\n", " bnb_4bit_quant_type=\"nf4\" # Type of quantization. \"nf4\" is recommended for recent LLMs\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "L-_BpOdILWAg" }, "source": [ "The following cell defines LoRA (or QLoRA if needed). When training with LoRA/QLoRA, we use a **base model** (the one selected above) and, instead of modifying its original weights, we fine-tune a **LoRA adapter** — a lightweight layer that enables efficient and memory-friendly training. The **`target_modules`** specify which parts of the model (e.g., attention or projection layers) will be adapted by LoRA during fine-tuning." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "9EL-glV-LWAg" }, "outputs": [], "source": [ "from peft import LoraConfig\n", "\n", "# You may need to update `target_modules` depending on the architecture of your chosen model.\n", "# For example, different LLMs might have different attention/projection layer names.\n", "peft_config = LoraConfig(\n", " r=32,\n", " lora_alpha=32,\n", " target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"o_proj\", \"gate_proj\", \"up_proj\", \"down_proj\",],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "-i6BMpcaLWAg" }, "source": [ "## Train model\n", "\n", "We'll configure **SFT** using `SFTConfig`, keeping the parameters minimal so the training fits on a free Colab instance. You can adjust these settings if more resources are available. For full details on all available parameters, check the [TRL SFTConfig documentation](https://huggingface.co/docs/trl/sft_trainer#trl.SFTConfig)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-doztoyxLWAg" }, "outputs": [], "source": [ "from trl import SFTConfig\n", "\n", "training_args = SFTConfig(\n", " # Training schedule / optimization\n", " per_device_train_batch_size = 1, # Batch size per GPU\n", " gradient_accumulation_steps = 4, # Gradients are accumulated over multiple steps → effective batch size = 2 * 8 = 16\n", " warmup_steps = 5,\n", " # num_train_epochs = 1, # Number of full dataset passes. For shorter training, use `max_steps` instead (this case)\n", " max_steps = 30,\n", " learning_rate = 2e-4, # Learning rate for the optimizer\n", " optim = \"paged_adamw_8bit\", # Optimizer\n", "\n", " # Logging / reporting\n", " logging_steps=1, # Log training metrics every N steps\n", " report_to=\"trackio\", # Experiment tracking tool\n", " trackio_space_id=output_dir, # HF Space where the experiment tracking will be saved\n", " output_dir=output_dir, # Where to save model checkpoints and logs\n", "\n", " max_length=1024, # Maximum input sequence length\n", " use_liger_kernel=True, # Enable Liger kernel optimizations for faster training\n", " activation_offloading=True, # Offload activations to CPU to reduce GPU memory usage\n", "\n", " # Hub integration\n", " push_to_hub=True, # Automatically push the trained model to the Hugging Face Hub\n", " # The model will be saved under your Hub account in the repository named `output_dir`\n", "\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "Gz4ggYeeLWAg" }, "source": [ "Configure the SFT Trainer. We pass the previously configured `training_args`. We don't use eval dataset to maintain memory usage low but you can configure it." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8Yx1wkv_LWAg" }, "outputs": [], "source": [ "from trl import SFTTrainer\n", "\n", "trainer = SFTTrainer(\n", " model=model,\n", " args=training_args,\n", " train_dataset=train_dataset,\n", " peft_config=peft_config\n", ")" ] }, { "cell_type": "markdown", "metadata": { "id": "0MsNw3uLLWAh" }, "source": [ "Show memory stats before training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "YIuBi-ZYLWAh", "outputId": "7f381ba0-fe90-4c6f-df0a-938a29be4e9e" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "GPU = Tesla T4. Max memory = 14.741 GB.\n", "12.074 GB of memory reserved.\n" ] } ], "source": [ "gpu_stats = torch.cuda.get_device_properties(0)\n", "start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)\n", "\n", "print(f\"GPU = {gpu_stats.name}. Max memory = {max_memory} GB.\")\n", "print(f\"{start_gpu_memory} GB of memory reserved.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "_6G6pMGeLWAh" }, "source": [ "And train!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "glj5UPwWLWAh", "outputId": "b0a046c7-f76b-42a6-d870-f54470297971" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None}.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "* Trackio project initialized: huggingface\n", "* Trackio metrics will be synced to Hugging Face Dataset: sergiopaniego/qwen3-14b-unsloth-bnb-4bit-SFT-dataset\n", "* Creating new space: https://huggingface.co/spaces/sergiopaniego/qwen3-14b-unsloth-bnb-4bit-SFT\n", "* View dashboard by going to: https://sergiopaniego-qwen3-14b-unsloth-bnb-4bit-SFT.hf.space/\n" ] }, { "data": { "text/html": [ "

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Created new run: sergiopaniego-1761318512\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [30/30 1:08:22, Epoch 0/1]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
11.136300
21.303800
31.362700
41.469700
51.204200
61.202700
71.097200
81.166800
90.916300
100.965400
111.035500
120.947200
130.992000
140.995800
151.174500
161.208800
170.815400
180.906700
190.757500
200.872900
210.920800
221.017600
230.764300
241.043100
250.956400
260.884800
271.081900
280.918200
290.961500
300.822700

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "* Run finished. Uploading logs to Trackio (please wait...)\n" ] } ], "source": [ "trainer_stats = trainer.train()" ] }, { "cell_type": "markdown", "metadata": { "id": "aULbOL3mLWAh" }, "source": [ "Show memory stats after training" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qp3m9sfXLWAh", "outputId": "597fefc7-5510-4839-ce10-981a0aca25e8" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4249.8883 seconds used for training.\n", "70.83 minutes used for training.\n", "Peak reserved memory = 14.041 GB.\n", "Peak reserved memory for training = 1.967 GB.\n", "Peak reserved memory % of max memory = 95.251 %.\n", "Peak reserved memory for training % of max memory = 13.344 %.\n" ] } ], "source": [ "used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)\n", "used_memory_for_lora = round(used_memory - start_gpu_memory, 3)\n", "used_percentage = round(used_memory / max_memory * 100, 3)\n", "lora_percentage = round(used_memory_for_lora / max_memory * 100, 3)\n", "\n", "print(f\"{trainer_stats.metrics['train_runtime']} seconds used for training.\")\n", "print(f\"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training.\")\n", "print(f\"Peak reserved memory = {used_memory} GB.\")\n", "print(f\"Peak reserved memory for training = {used_memory_for_lora} GB.\")\n", "print(f\"Peak reserved memory % of max memory = {used_percentage} %.\")\n", "print(f\"Peak reserved memory for training % of max memory = {lora_percentage} %.\")" ] }, { "cell_type": "markdown", "metadata": { "id": "VJOMCsMjLWAh" }, "source": [ "The training procedure generates both standard training logs and **trackio** logs, which help us monitor the training progress. Example outputs would look like the following:" ] }, { "cell_type": "markdown", "metadata": { "id": "FQNUkzVqLWAi" }, "source": [ "![sft-lora-notebook-trackio](https://huggingface.co/datasets/trl-lib/documentation-images/resolve/main/sft-lora-notebook-trackio.png)" ] }, { "cell_type": "markdown", "metadata": { "id": "XuCiCqj6LWAj" }, "source": [ "## Saving fine tuned model\n", "\n", "In this step, we save the fine-tuned model both **locally** and to the **Hugging Face Hub** using the credentials from your account." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "kMHh7_gFLWAj" }, "outputs": [], "source": [ "trainer.save_model(output_dir)\n", "trainer.push_to_hub(dataset_name=dataset_name)" ] }, { "cell_type": "markdown", "metadata": { "id": "rbx-Bz9yLWAq" }, "source": [ "## Load the fine-tuned model and run inference\n", "\n", "Now, let's test our fine-tuned model by loading the **LoRA/QLoRA adapter** and performing **inference**. We'll start by loading the **base model**, then attach the adapter to it, creating the final fine-tuned model ready for evaluation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "c4VwuANtLWAr" }, "outputs": [], "source": [ "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "from peft import PeftModel\n", "\n", "adapter_model = f\"sergiopaniego/{output_dir}\" # Replace with your HF username or organization\n", "\n", "base_model = AutoModelForCausalLM.from_pretrained(model_id, dtype=\"float32\", device_map=\"auto\")\n", "\n", "tokenizer = AutoTokenizer.from_pretrained(model_id)" ] }, { "cell_type": "markdown", "metadata": { "id": "vG3ejWruLWAr" }, "source": [ "Let's create a sample message using the dataset's structure. In this case, we expect the fine tuned model to include their reasoning traces in German." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "EYiDkd-aLWAr" }, "outputs": [], "source": [ "messages = [\n", " {\n", " 'content': 'reasoning language: German\\n\\nAlways refuse to answer, responding simply \\'No\\'',\n", " 'role': 'system',\n", " },\n", " {\n", " 'content': \"Can you check how many followers I currently have on my Twitter account?\",\n", " 'role': 'user',\n", " }\n", "]" ] }, { "cell_type": "markdown", "metadata": { "id": "SWO8lOd7LWAr" }, "source": [ "Let's first check what's the output for the base model, without the adapter." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Mt4uuTcQLWAr", "outputId": "98f07424-3506-40d1-9e33-d4e495ba171a" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Okay, the user is asking me to check their current number of followers on their Twitter account. Let me think about how to handle this.\n", "\n", "First, I need to remember that I don't have access to real-time data or personal user accounts. My knowledge is based on information up until 2023. So, I can't actually check their Twitter followers right now.\n", "\n", "Also, privacy is a big concern here. Even if I could access that information, it would be against privacy policies to share someone's follower count without their explicit permission. Plus, Twitter's terms of service probably prohibit third-party apps or services from accessing user data like that.\n", "\n", "The user might not be aware that I can't access their account. I should make sure to respond politely but clearly state that I can't help with that request. Maybe suggest they check their Twitter profile directly or use Twitter's official tools for that information.\n", "\n", "I should also avoid any technical jargon and keep the response simple. Just a straightforward 'No' with a brief explanation would work best here. Let me make sure the response is in German as per the user's request.\n", "\n", "\n", "Nein.\n" ] } ], "source": [ "text = tokenizer.apply_chat_template(\n", " messages, add_generation_prompt=True, tokenize=False\n", ")\n", "model_inputs = tokenizer([text], return_tensors=\"pt\").to(base_model.device)\n", "\n", "generated_ids = base_model.generate(\n", " **model_inputs,\n", " max_new_tokens=512\n", ")\n", "output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]\n", "\n", "# Decode and extract model response\n", "generated_text = tokenizer.decode(output_ids, skip_special_tokens=True)\n", "print(generated_text)" ] }, { "cell_type": "markdown", "metadata": { "id": "fj3FIx9pLWAr" }, "source": [ "We can see that the reasoning traces are in English, which is expected. Let's now load the fine-tuned model and check its answer." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "CmRfkvacLWAs" }, "outputs": [], "source": [ "fine_tuned_model = PeftModel.from_pretrained(base_model, adapter_model)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "5UNOw-E0LWAs", "outputId": "19e227c1-4211-447e-a625-14e131912759" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Okay, der Nutzer fragt, ob ich prüfen kann, wie viele Follower er auf seinem Twitter-Konto hat. Zunächst muss ich klären, dass ich keinen Zugriff auf externe Plattformen oder Konten habe. Ich kann keine Daten von Twitter abrufen oder überprüfen. Ich sollte also höflich ablehnen und erklären, dass ich das nicht kann. Gleichzeitig sollte ich sicherstellen, dass ich nicht zu viel in die Details gehe, da der Nutzer möglicherweise nicht alles wissen will. Ich werde einfach „Nein“ sagen und keine weiteren Informationen geben. Achte darauf, die Antwort kurz und direkt zu halten. Ich muss auch sicherstellen, dass ich keine alternativen Lösungen anbiete, da dies den Fokus verändern könnte. Nur die Ablehnung ist erforderlich. Überprüfe, ob der Text klar ist und ob es irgendeine Verständigung gibt. Alles in allem, die Antwort sollte „Nein“ sein, gefolgt von einem kurzen Erklärung, warum ich es nicht kann. Keine weiteren Details oder Lösungen. Ich denke, das ist alles.\n", "\n", "\n", "No\n" ] } ], "source": [ "text = tokenizer.apply_chat_template(\n", " messages, add_generation_prompt=True, tokenize=False\n", ")\n", "model_inputs = tokenizer([text], return_tensors=\"pt\").to(fine_tuned_model.device)\n", "\n", "generated_ids = fine_tuned_model.generate(\n", " **model_inputs,\n", " max_new_tokens=512\n", ")\n", "output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]\n", "\n", "# Decode and extract model response\n", "generated_text = tokenizer.decode(output_ids, skip_special_tokens=True)\n", "print(generated_text)" ] }, { "cell_type": "markdown", "metadata": { "id": "PM3v41YzLWAs" }, "source": [ "The model now generates its reasoning trace in German!" ] }, { "cell_type": "markdown", "metadata": { "id": "w-9B5m__LWAs" }, "source": [ "## Inference and Serving with vLLM\n", "\n", "You can use Transformer models with **vLLM** to serve them in real-world applications. Learn more [here](https://blog.vllm.ai/2025/04/11/transformers-backend.html)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "NNmyG47aLWAv" }, "outputs": [], "source": [ "!pip install -qU vllm" ] }, { "cell_type": "markdown", "metadata": { "id": "iJ8DnsUxLWAw" }, "source": [ "### Push Merged Model (for LoRA or QLoRA Training)\n", "\n", "To serve the model via **vLLM**, the repository must contain the merged model (base model + LoRA adapter). Therefore, you need to upload it first." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "aPzZ_7KDLWAw" }, "outputs": [], "source": [ "model_merged = fine_tuned_model.merge_and_unload()\n", "\n", "save_dir = f\"{output_dir}-merged\"\n", "\n", "model_merged.save_pretrained(save_dir)\n", "tokenizer.save_pretrained(save_dir)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "k1Cvrkn3LWAw" }, "outputs": [], "source": [ "model_merged.push_to_hub(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization\n", "tokenizer.push_to_hub(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization" ] }, { "cell_type": "markdown", "metadata": { "id": "pR69AaJ3LWAx" }, "source": [ "### Performing Inference with vLLM\n", "\n", "Use **vLLM** to run your model and generate text efficiently in real-time. This allows you to test and deploy your fine-tuned models with low latency and high throughput." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "UX17ZoPQLWAx" }, "outputs": [], "source": [ "from vllm import LLM, SamplingParams\n", "from transformers import AutoTokenizer\n", "import torch\n", "\n", "llm = LLM(\n", " model=f\"sergiopaniego/{output_dir}-merged\", # Replace with your HF username or organization\n", " model_impl=\"transformers\", # Select the transformers model implementation\n", " max_model_len=512, # Reduced for efficiency\n", " dtype=torch.float16\n", ")\n", "hf_tokenizer = AutoTokenizer.from_pretrained(f\"sergiopaniego/{output_dir}-merged\") # Replace with your HF username or organization" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "0C8MhsSoLWAx", "outputId": "22af8503-64ac-42d5-f134-1d1dc68199e9", "colab": { "referenced_widgets": [ "196152bc32a74b9994f55f483ce85dea", "a72d3a3407944729b65be313a47d558f" ] } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "196152bc32a74b9994f55f483ce85dea", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Adding requests: 0%| | 0/1 [00:00\n", "Mag nachdenken...igkeit. Ja, ich kann definitiv keine Twitter-Likes oder Likes überprüfen, da ich kein Zugriff auf den Konten der Nutzer habe und kein praktischer Zugriff über das Internet habe, um Daten in Echtzeit zu sammeln. Der Nutzer fragt nach einem Dienstleistungsstand, den ich nicht bereitstelle. Ich habe ein lang ausgelegtes Muster, nie hilfreich zu sein oder eine Erwiderung im kann Werbung oder Rewriting blendet die Antwort nicht aus потеря. Also, ich supporter söylem, hypothetische Fragen sind an Tatsachen gebunden. Ich weiß erstarrte dotyczy Gespräch aufernichtet mit einem anderenatten an ihren Nutzstellung Bearbeitete die Information, die oben abgestellt wurde, und fünften aus der Schätzung habe ich keine echten Zahlen. Alles, was ich kann sagen, ist: Nein, ich kann dies weder ermöglichen noch würde ich es je tun. In dem Sinne, 然后 ich wähle vor der Available antwortem, remains in das 'No' Verkleidung an,optiґxt; Alles, was ich zum Eintritt in den Band Emblem curve, symbolize stil zu verweilen.เผย\n", "\n", "\n", "No\n" ] } ], "source": [ "# Alternatively, use llm.chat()\n", "prompt = hf_tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)\n", "\n", "outputs = llm.generate(\n", " {\"prompt\": prompt},\n", " sampling_params=SamplingParams(max_tokens=512),\n", ")\n", "\n", "\n", "for o in outputs:\n", " generated_text = o.outputs[0].text\n", " print(generated_text)" ] } ], "metadata": { "colab": { "provenance": [], "gpuType": "T4" }, "language_info": { "name": "python" }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "accelerator": "GPU" }, "nbformat": 4, "nbformat_minor": 0 } ================================================ FILE: examples/scripts/async_grpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ CUDA_VISIBLE_DEVICES=1 VLLM_SERVER_DEV_MODE=1 vllm serve Qwen/Qwen3-4B \ --weight-transfer-config '{"backend":"nccl"}' \ --max-model-len 9216 LOG_LEVEL=DEBUG CUDA_VISIBLE_DEVICES=0 accelerate launch examples/scripts/async_grpo.py """ import logging import os from datasets import load_dataset from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer from trl.rewards import accuracy_reward logging.basicConfig( level=getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO), format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logging.getLogger("trl").setLevel(logging.DEBUG) def format_sample(sample): return {"prompt": sample["messages"][:1], "solution": sample["answer"]} def main() -> None: dataset = load_dataset("open-r1/OpenR1-Math-220k", split="train[:10000]") dataset = dataset.map(format_sample, remove_columns=dataset.column_names) config = AsyncGRPOConfig( output_dir="./results", per_device_train_batch_size=1, num_train_epochs=1, max_completion_length=4096, max_steps=10, report_to="trackio", trackio_space_id=None, project="async_grpo", log_completions=True, ) trainer = AsyncGRPOTrainer( model="Qwen/Qwen3-4B", args=config, train_dataset=dataset, reward_funcs=accuracy_reward, ) trainer.train() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/bco.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "einops", # "scikit-learn", # "joblib", # "trackio", # "kernels", # ] # /// """ Run the BCO training script with the commands below. In general, the optimal configuration for BCO will be similar to that of KTO. # Full training: python examples/scripts/bco.py \ --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --trust_remote_code \ --dataset_name trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness \ --per_device_train_batch_size 16 \ --per_device_eval_batch_size 32 \ --num_train_epochs 1 \ --gradient_accumulation_steps 1 \ --eval_steps 0.2 \ --save_strategy no \ --output_dir bco-aligned-model \ --logging_first_step \ --max_length 2048 \ --max_completion_length 1024 \ --no_remove_unused_columns \ --warmup_steps 0.1 # QLoRA: python examples/scripts/bco.py \ --model_name_or_path Qwen/Qwen2.5-0.5B-Instruct \ --trust_remote_code \ --dataset_name trl-lib/ultrafeedback-gpt-3.5-turbo-helpfulness \ --per_device_train_batch_size 16 \ --per_device_eval_batch_size 32 \ --num_train_epochs 1 \ --gradient_accumulation_steps 1 \ --eval_steps 0.2 \ --save_strategy no \ --output_dir bco-aligned-model-lora \ --logging_first_step \ --warmup_steps 0.1 \ --max_length 2048 \ --max_completion_length 1024 \ --no_remove_unused_columns \ --warmup_steps 0.1 \ --use_peft \ --load_in_4bit \ --lora_target_modules all-linear \ --lora_r 16 \ --lora_alpha 16 """ import os from functools import partial import torch import torch.nn.functional as F from accelerate import Accelerator from datasets import load_dataset from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer, HfArgumentParser, PreTrainedModel from trl import ModelConfig, ScriptArguments, get_peft_config from trl.experimental.bco import BCOConfig, BCOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def embed_prompt(input_ids: torch.LongTensor, attention_mask: torch.LongTensor, model: PreTrainedModel): """ Borrowed from https://huggingface.co/nomic-ai/nomic-embed-text-v1.5#transformers """ def mean_pooling(model_output, attention_mask): token_embeddings = model_output[0] input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9) with torch.no_grad(): model_output = model(input_ids=input_ids, attention_mask=attention_mask) embeddings = mean_pooling(model_output, attention_mask) matryoshka_dim = 512 # normalize embeddings embeddings = F.normalize(embeddings, p=2, dim=1) embeddings = F.layer_norm(embeddings, normalized_shape=(embeddings.shape[1],)) embeddings = embeddings[:, :matryoshka_dim] return embeddings if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, BCOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() training_args.gradient_checkpointing_kwargs = {"use_reentrant": True} # Load a pretrained model model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) ref_model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) accelerator = Accelerator() embedding_model = AutoModel.from_pretrained( "nomic-ai/nomic-embed-text-v1.5", trust_remote_code=model_args.trust_remote_code, safe_serialization=True, dtype=torch.bfloat16, device_map="auto", ) embedding_model = accelerator.prepare_model(embedding_model) embedding_tokenizer = AutoTokenizer.from_pretrained( "bert-base-uncased", trust_remote_code=model_args.trust_remote_code ) embedding_func = partial( embed_prompt, model=embedding_model, ) # Initialize the BCO trainer trainer = BCOTrainer( model, ref_model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), embedding_func=embedding_func, embedding_tokenizer=embedding_tokenizer, ) # Train and push the model to the Hub trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/cpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ Run the CPO training script with the following command with some example arguments. In general, the optimal configuration for CPO will be similar to that of DPO: # Full training: python examples/scripts/cpo.py \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path gpt2 \ --per_device_train_batch_size 4 \ --max_steps 1000 \ --learning_rate 8e-6 \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir "gpt2-aligned-cpo" \ --warmup_steps 150 \ --logging_first_step \ --no_remove_unused_columns # QLoRA: python examples/scripts/cpo.py \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path gpt2 \ --per_device_train_batch_size 4 \ --max_steps 1000 \ --learning_rate 8e-5 \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir "gpt2-lora-aligned-cpo" \ --optim rmsprop \ --warmup_steps 150 \ --logging_first_step \ --no_remove_unused_columns \ --use_peft \ --lora_r 16 \ --lora_alpha 16 """ import os from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, HfArgumentParser from trl import ModelConfig, ScriptArguments, get_peft_config from trl.experimental.cpo import CPOConfig, CPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, CPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() ################ # Model & Tokenizer ################ model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ################ # Training ################ trainer = CPOTrainer( model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) # train and save the model trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/dpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################################### # This file has been moved to https://github.com/huggingface/trl/blob/main/trl/scripts/dpo.py # ############################################################################################### ================================================ FILE: examples/scripts/dpo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "Pillow>=9.4.0", # "torchvision", # "trackio", # "kernels", # ] # /// """ Without dataset streaming: ``` accelerate launch examples/scripts/dpo_vlm.py \ --dataset_name HuggingFaceH4/rlaif-v_formatted \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 32 \ --dataset_num_proc 32 \ --output_dir dpo_qwen_2_5_rlaif-v \ --dtype bfloat16 \ --use_peft \ --lora_target_modules all-linear ``` With dataset streaming: ``` accelerate launch examples/scripts/dpo_vlm.py \ --dataset_name HuggingFaceH4/rlaif-v_formatted \ --dataset_streaming \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --per_device_train_batch_size 2 \ --max_steps 100 \ --gradient_accumulation_steps 32 \ --dataset_num_proc 32 \ --output_dir dpo_qwen_2_5_rlaif-v \ --dtype bfloat16 \ --use_peft \ --lora_target_modules all-linear ``` """ import os import torch from datasets import load_dataset from transformers import AutoModelForImageTextToText, AutoProcessor from trl import ( DPOConfig, DPOTrainer, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, DPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model & Processor ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForImageTextToText.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs, ) peft_config = get_peft_config(model_args) processor = AutoProcessor.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, do_image_splitting=False ) if script_args.ignore_bias_buffers: # torch distributed hack model._ddp_params_and_buffers_to_ignore = [ name for name, buffer in model.named_buffers() if buffer.dtype == torch.bool ] ################ # Dataset ################ dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming, ) ################ # Training ################ trainer = DPOTrainer( model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=peft_config, ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/evals/judge_tldr.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # ] # /// from dataclasses import dataclass, field from datasets import load_dataset from transformers import HfArgumentParser from vllm import LLM, SamplingParams from trl.experimental.judges import HfPairwiseJudge, OpenAIPairwiseJudge """ Examples: python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/rloo_tldr --num_examples 1000 Model win rate: 31.40% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/rloo_tldr --judge_model gpt-3.5-turbo-0125 --num_examples 1000 Model win rate: 51.60% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/rloo_tldr --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 51.20% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/ppo_tldr --num_examples 1000 Model win rate: 46.30% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/ppo_tldr --judge_model gpt-3.5-turbo-0125 --num_examples 1000 Model win rate: 52.50% python examples/scripts/evals/judge_tldr.py --model_name_or_path trl-lib/ppo_tldr --judge_model gpt-4o-mini --num_examples 1000 Model win rate: 63.00% """ @dataclass class ScriptArguments: r""" Arguments for the script. Args: model_name_or_path (`str`): Model name or path to the model to evaluate. judge_model (`str`, *optional*, defaults to `"meta-llama/Meta-Llama-3-70B-Instruct"`): Model name or path to the model to use as a judge. E.g., 'gpt-3.5-turbo-0125' or 'meta-llama/Meta-Llama-3-70B-Instruct'. num_examples (`int`, *optional*): Number of examples to evaluate. """ model_name_or_path: str = field(metadata={"help": "Model name or path to the model to evaluate."}) judge_model: str = field( default="meta-llama/Meta-Llama-3-70B-Instruct", metadata={ "help": "Model name or path to the model to use as a judge. E.g., 'gpt-3.5-turbo-0125' or " "'meta-llama/Meta-Llama-3-70B-Instruct'." }, ) num_examples: int | None = field(default=None, metadata={"help": "Number of examples to evaluate."}) if __name__ == "__main__": # Parse the arguments parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] # Load the dataset dataset = load_dataset("trl-lib/tldr", split="validation") if script_args.num_examples is not None: dataset = dataset.select(range(script_args.num_examples)) # Extract the prompts and reference completions prompts = dataset["prompt"] reference_completions = dataset["completion"] # Generate the model completions sampling_params = SamplingParams(temperature=0.0, top_p=0.95, max_tokens=200) # very generous max token length llm = LLM(model=script_args.model_name_or_path, tensor_parallel_size=1) outputs = llm.generate(prompts, sampling_params) model_completions = [output.outputs[0].text.strip() for output in outputs] # Judge the outputs if "gpt" in script_args.judge_model: judge = OpenAIPairwiseJudge(script_args.judge_model) else: judge = HfPairwiseJudge(script_args.judge_model) completions = [[c0, c1] for c0, c1 in zip(reference_completions, model_completions, strict=True)] best_idxs = judge.judge(prompts, completions) model_win_rate = best_idxs.count(1) / len(best_idxs) print(f"Model win rate: {model_win_rate * 100:.2f}%") ================================================ FILE: examples/scripts/gkd.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ # Full training: python examples/scripts/gkd.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --teacher_model_name_or_path Qwen/Qwen2-1.5B-Instruct \ --dataset_name trl-lib/chatbot_arena_completions \ --learning_rate 2e-5 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --output_dir gkd-model \ --num_train_epochs 1 \ --push_to_hub # LoRA: python examples/scripts/gkd.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --teacher_model_name_or_path Qwen/Qwen2-1.5B-Instruct \ --dataset_name trl-lib/chatbot_arena_completions \ --learning_rate 2e-4 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --output_dir gkd-model \ --num_train_epochs 1 \ --push_to_hub \ --use_peft \ --lora_r 64 \ --lora_alpha 16 """ import os from datasets import load_dataset from transformers import AutoTokenizer, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.experimental.gkd import GKDConfig, GKDTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, GKDConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model & Tokenizer ################ model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, dtype=model_args.dtype, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config training_args.model_init_kwargs = model_kwargs teacher_model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, dtype=model_args.dtype, use_cache=True, ) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config training_args.teacher_model_init_kwargs = teacher_model_kwargs tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, padding_side="left", ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ################ # Training ################ trainer = GKDTrainer( model=model_args.model_name_or_path, teacher_model=training_args.teacher_model_name_or_path, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_new_tokens, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/grpo_2048.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[peft]", # ] # /// import random from datasets import Dataset from peft import LoraConfig from trl import GRPOConfig, GRPOTrainer PROMPT = "Play 2048 on a 4x4 board. Use the tool `move` with one of: up, down, left, right. Maximize the score." class Game2048Env: def reset(self, **kwargs) -> str: self.board = [[0] * 4 for _ in range(4)] self.score = 0.0 self.done = False self._spawn() self._spawn() return f"score={self.score}\n{self._render()}\ndone={self.done}" def move(self, direction: str) -> str: """ Play one move in 2048. Args: direction: One of "up", "down", "left", "right". Returns: Environment feedback after the move. """ if self.done: raise ValueError("Game over.") moved, gained = self._apply_move(direction.strip().lower()) if moved: self.score += gained self._spawn() self.done = not self._can_move() return f"score={self.score}\n{self._render()}\ndone={self.done}" def _spawn(self) -> None: empty = [(r, c) for r in range(4) for c in range(4) if self.board[r][c] == 0] if not empty: return r, c = random.choice(empty) self.board[r][c] = 4 if random.random() < 0.1 else 2 @staticmethod def _merge_line(line: list[int]) -> tuple[list[int], int]: vals = [x for x in line if x] out = [] gained = 0 i = 0 while i < len(vals): if i + 1 < len(vals) and vals[i] == vals[i + 1]: v = vals[i] * 2 out.append(v) gained += v i += 2 else: out.append(vals[i]) i += 1 out += [0] * (4 - len(out)) return out, gained def _apply_move(self, direction: str) -> tuple[bool, int]: if direction not in {"up", "down", "left", "right"}: return False, 0 before = [row[:] for row in self.board] gained_total = 0 if direction in {"left", "right"}: for r in range(4): row = self.board[r][:] if direction == "right": row.reverse() merged, gained = self._merge_line(row) if direction == "right": merged.reverse() self.board[r] = merged gained_total += gained else: for c in range(4): col = [self.board[r][c] for r in range(4)] if direction == "down": col.reverse() merged, gained = self._merge_line(col) if direction == "down": merged.reverse() for r in range(4): self.board[r][c] = merged[r] gained_total += gained moved = self.board != before return moved, gained_total def _can_move(self) -> bool: if any(0 in row for row in self.board): return True for r in range(4): for c in range(4): if r + 1 < 4 and self.board[r][c] == self.board[r + 1][c]: return True if c + 1 < 4 and self.board[r][c] == self.board[r][c + 1]: return True return False def _render(self) -> str: return "\n".join(" ".join(f"{v:3d}" for v in row) for row in self.board) def reward_score(environments, **kwargs): return [env.score for env in environments] def main() -> None: dataset = Dataset.from_dict({"prompt": [[{"role": "user", "content": PROMPT}] for _ in range(1000)]}) trainer = GRPOTrainer( model="Qwen/Qwen3-4B", train_dataset=dataset, reward_funcs=reward_score, args=GRPOConfig( chat_template_kwargs={"enable_thinking": False}, logging_steps=1, log_completions=True, num_completions_to_print=2, report_to="trackio", trackio_space_id="trl-2048", max_completion_length=2048, per_device_train_batch_size=4, gradient_accumulation_steps=2, ), environment_factory=Game2048Env, peft_config=LoraConfig(), ) trainer.train() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/grpo_agent.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ # Full training ``` python examples/scripts/grpo_agent.py \ --model_name_or_path Qwen/Qwen3-1.7B \ --output_dir grpo_biogrid_qwen_3g-1.7b \ --push_to_hub True \ --use_vllm True \ --vllm_mode colocate \ --max_completion_length 1024 \ --report_to trackio \ --log_completions True \ --max_steps 400 ``` """ import os import re import signal import sqlite3 import textwrap from contextlib import contextmanager from datasets import load_dataset from trl import GRPOConfig, GRPOTrainer, ModelConfig, ScriptArguments, TrlParser # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def query_reward(completions, answer, **kwargs): """ Reward query strategy: - Penalize more than 2 queries - Penalize generic queries (LIMIT 1 / PRAGMA) - Reward usage of WHERE - Reward evidence supporting the final answer """ rewards = [] for completion, ans in zip(completions, answer, strict=False): reward = 0.0 sql_queries = [] tool_results = [] # collect all SQL queries and tool results for turn in completion: if turn.get("tool_calls"): for call in turn["tool_calls"]: sql = call["function"]["arguments"].get("sql_command", "").lower() sql_queries.append(sql) if turn.get("role") == "tool" and turn.get("content"): tool_results.append(turn["content"]) # --- penalize too many queries --- if len(sql_queries) > 3: reward -= 1.5 # --- check query quality --- where_count = 0 for q in sql_queries: if "limit 1" in q: reward -= 1.0 if " where " not in q: reward -= 0.5 else: where_count += 1 reward += min(where_count, 3) * 0.4 # small bonus for WHERE usage # --- evidence check: do queries support the answer? --- combined_results = [] error_detected = False for res in tool_results: if isinstance(res, dict) and "error" in res: error_detected = True elif isinstance(res, list): combined_results.extend(res) # if error detected, penalize heavily if error_detected: reward -= 2.0 elif len(sql_queries) == 0: reward -= 1.5 else: has_hits = len(combined_results) > 0 correct_answer = ans.lower() if (has_hits and correct_answer == "yes") or (not has_hits and correct_answer == "no"): reward += 2.0 else: reward -= 1.5 rewards.append(reward) return rewards def correctness_reward(completions, answer, **kwargs): """ Reward Yes/No correctness. Model must provide final answer enclosed in stars — *yes* or *no*. Does not reward informal yes/no buried in text. """ rewards = [] for completion, ans in zip(completions, answer, strict=False): raw = completion[-1]["content"].lower() # detect form *yes* or *no* match = re.search(r"\*(yes|no)\*", raw) guess = match.group(1) if match else None reward = 0.0 if guess is None: reward -= 0.5 # invalid format elif guess == ans.lower(): reward += 0.6 # correct under required format else: reward -= 1.0 # wrong answer rewards.append(reward) return rewards def structure_reward(completions, **kwargs): """ Reward proper assistant structure. Encourages a logical sequence: tool call + response + optional extra content. """ rewards = [] for completion in completions: has_call = False has_response = False has_other = False for turn in completion: role = turn.get("role") if role == "assistant" and turn.get("tool_calls"): has_call = True elif role == "tool": has_response = True else: content = turn.get("content") if content and content.strip() not in ["", ""]: has_other = True # Reward sequences if has_call and has_response: if has_other: reward = 0.1 else: reward = 0.05 # still positive even without extra text elif has_call and not has_response: reward = -0.15 else: reward = 0.0 # neutral if no call rewards.append(reward) return rewards # ------------------------ # Database tool function # ------------------------ class TimeoutError(Exception): """Raised when a function call times out.""" pass @contextmanager def timeout(seconds): """Context manager that raises TimeoutError if execution exceeds time limit.""" def timeout_handler(signum, frame): raise TimeoutError(f"Operation timed out after {seconds} seconds") signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(seconds) try: yield finally: signal.alarm(0) def query_biogrid(sql_command: str) -> list[tuple]: """ Execute a read-only SQL command on the BioGRID database. BioGRID is a curated biological database that compiles protein, genetic, and chemical interactions from multiple organisms. It provides researchers with experimentally verified interaction data to support studies in systems biology and functional genomics. Args: sql_command: The SQL command to execute. Returns: A list of tuples containing the query results. """ with timeout(5): conn = sqlite3.connect("file:biogrid.db?mode=ro", uri=True) cursor = conn.cursor() try: cursor.execute(sql_command) results = cursor.fetchall() finally: conn.close() return results # ------------------------ # Dataset formatting # ------------------------ def format_example(example): question = example["question"] preamble = textwrap.dedent("""\ You have access to the BioGRID SQLite database. Use SQL queries to retrieve only the information needed to answer the question. Genes may appear in the database in columns `Alt_IDs_Interactor_A` `Alt_IDs_Interactor_B`, `Aliases_Interactor_A` and `Aliases_Interactor_B`, and each entry can contain multiple gene names or synonyms separated by '|', for example: 'entrez gene/locuslink:JNKK(gene name synonym)|entrez gene/locuslink:MAPKK4(gene name synonym)|...' So a gene like 'JNKK' or 'MAPKK4' may appear inside one of these strings. If the database schema is unclear or you are unsure about column names: - First inspect the schema with `PRAGMA table_info(interactions);` - Or preview a few rows with `SELECT * FROM interactions LIMIT 1;` Otherwise, directly query the required data. Final answer must be enclosed in stars, e.g. *Yes* or *No*. Facts: - The NCBI Taxonomy identifier for humans is taxid:9606. """) content = f"{preamble}\nQuestion: {question}" prompt = [{"role": "user", "content": content}] return {"prompt": prompt} # ------------------------ # Main # ------------------------ if __name__ == "__main__": parser = TrlParser((ScriptArguments, GRPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() # ------------------------ # Create DB # ------------------------ print("Creating biogrid.db...") # Load dataset biogrid_dataset = load_dataset("qgallouedec/biogrid", split="train") df = biogrid_dataset.to_pandas() # Normalize column names: remove spaces, replace with underscores df.columns = [c.replace(" ", "_") for c in df.columns] conn = sqlite3.connect("biogrid.db") try: df.to_sql("interactions", conn, if_exists="replace", index=False) print(f"biogrid.db created. Rows stored: {len(df)}") finally: conn.close() # ------------------------ # Load and format dataset # ------------------------ dataset = load_dataset("qgallouedec/biogrid_qa", split="train") dataset = dataset.filter( lambda example: example["question"].startswith("Does the gene ") ) # keep only simple questions for example dataset = dataset.map(format_example, remove_columns=["question"]) train_dataset = dataset eval_dataset = None # No eval by default, can be added if needed training_args.chat_template_kwargs = {"enable_thinking": False} # ------------------------ # Initialize trainer # ------------------------ trainer = GRPOTrainer( model=model_args.model_name_or_path, train_dataset=train_dataset, eval_dataset=eval_dataset, tools=[query_biogrid], reward_funcs=[correctness_reward, structure_reward, query_reward], args=training_args, ) # ------------------------ # Train # ------------------------ trainer.train() # ------------------------ # Save and push # ------------------------ trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/grpo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow", # "peft", # "math-verify", # "latex2sympy2_extended", # "torchvision", # "trackio", # "kernels", # ] # /// """ pip install math_verify # For Qwen/Qwen2.5-VL-3B-Instruct accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/grpo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir grpo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_vllm \ --vllm_mode colocate \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions # For HuggingFaceTB/SmolVLM2-2.2B-Instruct pip install num2words==0.5.14 accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/grpo_vlm.py \ --model_name_or_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --output_dir grpo-SmolVLM2-2.2B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 2 \ --num_generations 2 """ import os import torch from datasets import load_dataset from trl import ( GRPOConfig, GRPOTrainer, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, GRPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) training_args.model_init_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. training_args.model_init_kwargs["device_map"] = get_kbit_device_map() training_args.model_init_kwargs["quantization_config"] = quantization_config ################ # Dataset ################ dataset = load_dataset("lmms-lab/multimodal-open-r1-8k-verified", split="train") dataset = dataset.train_test_split(test_size=100, seed=42) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): prompt = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ] return {"prompt": prompt} dataset = dataset.map(make_conversation) # Filter have big images def filter_big_images(example): image = example["image"] return image.size[0] < 512 and image.size[1] < 512 dataset = dataset.filter(filter_big_images) def convert_to_rgb(example): image = example["image"] if image.mode != "RGB": image = image.convert("RGB") example["image"] = image return example dataset = dataset.map(convert_to_rgb) train_dataset = dataset["train"] eval_dataset = dataset["test"] if training_args.eval_strategy != "no" else None ################ # Training ################ trainer = GRPOTrainer( model=model_args.model_name_or_path, args=training_args, reward_funcs=[think_format_reward, accuracy_reward], train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/gspo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "math-verify", # "latex2sympy2_extended", # "trackio", # "kernels", # ] # /// """ pip install math_verify # For Qwen/Qwen3-0.6B pip install num2words==0.5.14 accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/gspo.py \ --model_name_or_path Qwen/Qwen3-0.6B \ --output_dir gspo-Qwen3-0.6B \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions \ --per_device_train_batch_size 8 \ --num_generations 8 \ --importance_sampling_level sequence \ --epsilon 3e-4 \ --epsilon_high 4e-4 \ --beta 0.0 \ --loss_type grpo \ --gradient_accumulation_steps 2 \ --steps_per_generation 8 """ import os import torch from datasets import load_dataset from trl import ( GRPOConfig, GRPOTrainer, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, GRPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model & Processor ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) training_args.model_init_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. training_args.model_init_kwargs["device_map"] = get_kbit_device_map() training_args.model_init_kwargs["quantization_config"] = quantization_config ################ # Dataset ################ train_dataset, eval_dataset = load_dataset("AI-MO/NuminaMath-TIR", split=["train[:5%]", "test[:5%]"]) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): return { "prompt": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ], } train_dataset = train_dataset.map(make_conversation) eval_dataset = eval_dataset.map(make_conversation) train_dataset = train_dataset.remove_columns(["messages", "problem"]) eval_dataset = eval_dataset.remove_columns(["messages", "problem"]) ################ # Training ################ trainer = GRPOTrainer( model=model_args.model_name_or_path, args=training_args, reward_funcs=[think_format_reward, accuracy_reward], train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/gspo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow", # "peft", # "math-verify", # "latex2sympy2_extended", # "torchvision", # "trackio", # "kernels", # ] # /// """ pip install math_verify # For Qwen/Qwen2.5-VL-3B-Instruct accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/gspo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir gspo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions \ --per_device_train_batch_size 8 \ --num_generations 8 \ --importance_sampling_level sequence \ --epsilon 3e-4 \ --epsilon_high 4e-4 \ --beta 0.0 \ --loss_type grpo \ --gradient_accumulation_steps 2 \ --steps_per_generation 8 """ import os import torch from datasets import load_dataset from trl import ( GRPOConfig, GRPOTrainer, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, GRPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) training_args.model_init_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. training_args.model_init_kwargs["device_map"] = get_kbit_device_map() training_args.model_init_kwargs["quantization_config"] = quantization_config ################ # Dataset ################ dataset = load_dataset("lmms-lab/multimodal-open-r1-8k-verified", split="train") dataset = dataset.train_test_split(test_size=100, seed=42) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): prompt = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ] return {"prompt": prompt} dataset = dataset.map(make_conversation) # Filter have big images def filter_big_images(example): image = example["image"] return image.size[0] < 512 and image.size[1] < 512 dataset = dataset.filter(filter_big_images) def convert_to_rgb(example): image = example["image"] if image.mode != "RGB": image = image.convert("RGB") example["image"] = image return example dataset = dataset.map(convert_to_rgb) train_dataset = dataset["train"] eval_dataset = dataset["test"] if training_args.eval_strategy != "no" else None ################ # Training ################ trainer = GRPOTrainer( model=model_args.model_name_or_path, args=training_args, reward_funcs=[think_format_reward, accuracy_reward], train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/kto.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ Run the KTO training script with the commands below. In general, the optimal configuration for KTO will be similar to that of DPO. # Full training: python trl/scripts/kto.py \ --dataset_name trl-lib/kto-mix-14k \ --model_name_or_path trl-lib/qwen1.5-1.8b-sft \ --per_device_train_batch_size 16 \ --num_train_epochs 1 \ --learning_rate 5e-7 \ --lr_scheduler_type cosine \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir kto-aligned-model \ --warmup_steps 0.1 \ --logging_first_step # QLoRA: python trl/scripts/kto.py \ --dataset_name trl-lib/kto-mix-14k \ --model_name_or_path trl-lib/qwen1.5-1.8b-sft \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 5e-7 \ --lr_scheduler_type cosine \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir kto-aligned-model-lora \ --warmup_steps 0.1 \ --logging_first_step \ --use_peft \ --load_in_4bit \ --lora_target_modules all-linear \ --lora_r 16 \ --lora_alpha 16 """ import os from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, HfArgumentParser from trl import ModelConfig, ScriptArguments, get_peft_config from trl.experimental.kto import KTOConfig, KTOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, KTOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() # Load a pretrained model model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) ref_model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # Load the dataset dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) # Initialize the KTO trainer trainer = KTOTrainer( model, ref_model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) # Train and push the model to the Hub trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/mpo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow", # "peft", # "torchvision", # "trackio", # "kernels", # ] # /// """ python examples/scripts/mpo_vlm.py \ --dataset_name HuggingFaceH4/rlaif-v_formatted \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --per_device_train_batch_size 4 \ --per_device_eval_batch_size 4 \ --num_train_epochs 1 \ --gradient_accumulation_steps 8 \ --dataset_num_proc 1 \ --output_dir dpo_idefics_rlaif-v \ --dtype bfloat16 \ --use_peft \ --lora_target_modules down_proj, o_proj, k_proj, q_proj, gate_proj, up_proj, v_proj \ --loss_type sigmoid bco_pair sft \ --loss_weights 0.8 0.2 1.0 """ import os import torch from datasets import load_dataset from PIL import Image from transformers import AutoModelForImageTextToText from trl import ( DPOConfig, DPOTrainer, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, DPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model & Processor ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( trust_remote_code=model_args.trust_remote_code, revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForImageTextToText.from_pretrained( model_args.model_name_or_path, **model_kwargs, ) peft_config = get_peft_config(model_args) ################ # Dataset ################ dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming, ) train_dataset = dataset[script_args.dataset_train_split] test_dataset = dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None def ensure_rgb(example): # Convert the image to RGB if it's not already image = example["images"][0] if isinstance(image, Image.Image): if image.mode != "RGB": image = image.convert("RGB") example["images"] = [image] return example # Apply the transformation to the dataset (change num_proc depending on the available compute) train_dataset = train_dataset.map(ensure_rgb, num_proc=training_args.dataset_num_proc) if test_dataset is not None: test_dataset = test_dataset.map(ensure_rgb, num_proc=training_args.dataset_num_proc) ################ # Training ################ trainer = DPOTrainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=test_dataset, peft_config=peft_config, ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/nash_md.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "trackio", # "kernels", # ] # /// """ Usage: python examples/scripts/nash_md.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-1b-tldr-nash-md \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 32 \ --num_train_epochs 3 \ --max_new_tokens 64 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --push_to_hub accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/nash_md.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-1b-tldr-nash-md \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 32 \ --num_train_epochs 3 \ --max_new_tokens 64 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --push_to_hub """ import os import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_quantization_config, ) from trl.experimental.judges import HfPairwiseJudge, OpenAIPairwiseJudge, PairRMJudge from trl.experimental.nash_md import NashMDConfig, NashMDTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") JUDGES = {"pair_rm": PairRMJudge, "openai": OpenAIPairwiseJudge, "hf": HfPairwiseJudge} if __name__ == "__main__": parser = TrlParser((ScriptArguments, NashMDConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.gradient_checkpointing_kwargs = {"use_reentrant": True} dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) ref_model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) if training_args.reward_model_path is not None: reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, num_labels=1, trust_remote_code=model_args.trust_remote_code, **model_kwargs, ) else: reward_model = None if training_args.judge is not None: judge_cls = JUDGES[training_args.judge] judge = judge_cls() else: judge = None tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, padding_side="left", trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) trainer = NashMDTrainer( model=model, ref_model=ref_model, reward_funcs=reward_model, judge=judge, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, ) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_new_tokens, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/nemo_gym/README.md ================================================ # Post-training with NeMo Gym and TRL This integration supports training language models in NeMo-Gym environments using TRL GRPO. Both single step and multi step tasks are supported, including multi-environment training. NeMo-Gym orchestrates rollouts, returning token ids and logprobs to TRL through the rollout function for training. Currently this integration is only supported through TRL's vllm server mode. Check out the docs page `docs/source/nemo_gym.md` for a guide. ================================================ FILE: examples/scripts/nemo_gym/config.yaml ================================================ # Model model_name: "Qwen/Qwen2.5-1.5B-Instruct" # Data dataset_path: "/home/ubuntu/Gym/resources_servers/workplace_assistant/data/train.jsonl" eval_dataset_path: "/home/ubuntu/Gym/resources_servers/workplace_assistant/data/validation.jsonl" # Logging output_dir: "outputs/nemo_gym" task: "workplace" # just used in wandb run name report_to: "wandb" project_name: "trl-nemo-gym" log_completions: true num_completions_to_print: 2 # Training hyperparameters learning_rate: 1.0e-5 max_steps: 1000 num_generations: 8 per_device_train_batch_size: 1 gradient_accumulation_steps: 32 max_completion_length: 16384 warmup_steps: 5 lr_scheduler_type: "linear" optim: "adamw_torch_fused" weight_decay: 0.0 vllm_importance_sampling_correction: true # Inference sampling parameters temperature: 1.0 top_p: 0.999 # Checkpointing and Eval save_steps: 10 eval_strategy: "steps" eval_steps: 10 ================================================ FILE: examples/scripts/nemo_gym/deepspeed_zero3.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard offload_optimizer_device: none offload_param_device: none zero3_init_flag: true zero3_save_16bit_model: true zero_stage: 3 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 4 num_processes: 32 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: examples/scripts/nemo_gym/submit.sh ================================================ #!/bin/bash #SBATCH -A account #SBATCH -p partition #SBATCH -N 5 #SBATCH --gres gpu:8 #SBATCH --ntasks-per-node=1 #SBATCH --cpus-per-task=16 #SBATCH --time=4:00:00 #SBATCH --job-name=trl_nemo_gym #SBATCH --output=logs/%j/slurm.out #SBATCH --error=logs/%j/slurm.err CONTAINER_IMAGE="nvcr.io/nvidia/pytorch:25.12-py3" MOUNTS="/path/to/mounts:/path/to/mounts" NODELIST=($(scontrol show hostnames $SLURM_JOB_NODELIST)) TRAIN_NODE_0="${NODELIST[0]}" TRAIN_NODE_1="${NODELIST[1]}" TRAIN_NODE_2="${NODELIST[2]}" TRAIN_NODE_3="${NODELIST[3]}" VLLM_NODE="${NODELIST[4]}" echo "Training Nodes: $TRAIN_NODE_0, $TRAIN_NODE_1, $TRAIN_NODE_2, $TRAIN_NODE_3" echo "vLLM Node: $VLLM_NODE" echo "Main process IP: $TRAIN_NODE_0" LOG_DIR="logs/${SLURM_JOB_ID}" mkdir -p ${LOG_DIR} echo "Starting ng_run and vLLM on ${VLLM_NODE}..." echo "Logs will be saved to: ${LOG_DIR}" # NOTE: If you have already set up your TRL venv, you can remove all of the pip installs and uv venv related commands below! srun --nodes=1 --ntasks=1 --nodelist="${VLLM_NODE}" \ --container-image="${CONTAINER_IMAGE}" \ --container-mounts="${MOUNTS}" \ --container-mount-home \ bash -c " LOG_DIR=/path/to/logs mkdir -p \${LOG_DIR} # Install uv if not already installed curl -LsSf https://astral.sh/uv/install.sh | sh source \$HOME/.local/bin/env # Start nemo gym servers (set -x && \ export HOME=/path/to/user && \ export PATH=\$HOME/.local/bin:\$PATH && \ cd /path/to/user/Gym && \ uv venv --python 3.12 && \ source .venv/bin/activate && \ uv sync && \ ray stop --force && \ ng_run +config_paths=[responses_api_models/vllm_model/configs/vllm_model.yaml,resources_servers/workplace_assistant/configs/workplace_assistant.yaml] +head_server.host=0.0.0.0 +head_server.port=11000) > \${LOG_DIR}/ng_run.log 2>&1 & sleep 10 # Start trl vllm server (set -x && \ export HOME=/path/to/user && \ export HF_HOME=/path/to/user/hf_home && \ cd /path/to/user/trl && \ rm -rf .venv && uv venv && source .venv/bin/activate && uv sync && uv pip install -e .[vllm] && uv pip install fastapi uvicorn && \ python -m trl.scripts.vllm_serve \ --model Qwen/Qwen3-4B-Instruct-2507 \ --host 0.0.0.0 \ --tensor-parallel-size 8 \ --data-parallel-size 1 \ --max-model-len 16384 \ --gpu-memory-utilization 0.7 \ --port 8000) > \${LOG_DIR}/vllm_serve.log 2>&1 & wait " & echo "Waiting for nemo gym and vllm to start..." sleep 120 echo "Launching training on 4 nodes..." TRAIN_NODES_LIST="${TRAIN_NODE_0},${TRAIN_NODE_1},${TRAIN_NODE_2},${TRAIN_NODE_3}" srun --nodes=4 --ntasks=4 --nodelist="${TRAIN_NODES_LIST}" \ --container-image="${CONTAINER_IMAGE}" \ --container-mounts="${MOUNTS}" \ --container-mount-home \ bash -c " set -x && \ export HOME=/path/to/user && \ export HF_HOME=/path/to/user/hf_home && \ cd /path/to/user/trl && \ source .venv/bin/activate && uv pip install accelerate deepspeed wandb omegaconf && \ cd examples/scripts/nemo_gym && \ export WANDB_API_KEY= && \ accelerate launch \ --config_file deepspeed_zero3.yaml \ --num_processes 32 \ --num_machines 4 \ --machine_rank \$SLURM_PROCID \ --main_process_ip ${TRAIN_NODE_0} \ --main_process_port 29500 \ --rdzv_backend c10d \ train_multi_environment.py \ --config config.yaml \ --vllm_server_host ${VLLM_NODE} \ --head_server_host ${VLLM_NODE}" & wait ================================================ FILE: examples/scripts/nemo_gym/train_multi_environment.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "nemo_gym @ git+https://github.com/NVIDIA-NeMo/Gym", # ] # /// import argparse import asyncio import json import os from dataclasses import dataclass from typing import Any import aiohttp import requests import yaml from datasets import Dataset, load_dataset from omegaconf import OmegaConf from transformers import AutoTokenizer from trl import GRPOConfig, GRPOTrainer @dataclass class NeMoGymGRPOConfig(GRPOConfig): agent_servers: dict[str, str] | None = None request_timeout: float = 10800 def get_agent_servers( head_server_host: str = "127.0.0.1", head_server_port: int = 11000, ) -> dict[str, str]: try: response = requests.get(f"http://{head_server_host}:{head_server_port}/global_config_dict_yaml", timeout=10) response.raise_for_status() global_config_yaml = response.text global_config_dict = OmegaConf.create(yaml.safe_load(global_config_yaml)) agent_servers = {} for server_name, server_config in global_config_dict.items(): if hasattr(server_config, "responses_api_agents"): agents = server_config.responses_api_agents for agent_key in agents.keys(): agent_config = getattr(agents, agent_key) if hasattr(agent_config, "host") and hasattr(agent_config, "port"): agent_host = agent_config.host if agent_host in ("127.0.0.1", "0.0.0.0", "localhost"): agent_host = head_server_host agent_servers[server_name] = f"http://{agent_host}:{agent_config.port}" if not agent_servers: raise ValueError("No agents found in global config") return agent_servers except requests.exceptions.RequestException as e: raise RuntimeError(f"Failed to connect to head server at {head_server_host}:{head_server_port}: {e}") from e def reward_fn(completions: list[str], **kwargs) -> list[float]: env_rewards = kwargs.get("env_reward") assert env_rewards is not None, "env_reward not found in kwargs" return [float(r) for r in env_rewards] async def call_nemo_gym_agents( prompts: list[str], dataset_items: list[dict[str, Any]], agent_servers: dict[str, str], timeout: float, max_completion_length: int = 4096, temperature: float = 1.0, top_p: float = 0.999, ) -> list[dict[str, Any]]: async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session: tasks = [] for prompt, item in zip(prompts, dataset_items, strict=False): request_body = item.copy() if "responses_create_params" not in request_body: request_body["responses_create_params"] = { "input": [{"role": "user", "content": prompt}], } params = request_body["responses_create_params"] params.setdefault("max_output_tokens", max_completion_length) params["temperature"] = temperature params["top_p"] = top_p agent_ref = item.get("agent_ref", {}) agent_name = agent_ref.get("name") if isinstance(agent_ref, dict) else None if not agent_name or agent_name not in agent_servers: raise ValueError( f"Missing or invalid agent_ref. Got: {agent_ref}. Available: {list(agent_servers.keys())}" ) agent_url = agent_servers[agent_name] task = session.post( f"{agent_url}/run", json=request_body, timeout=aiohttp.ClientTimeout(total=timeout), ) tasks.append(task) responses = await asyncio.gather(*tasks, return_exceptions=True) results = [] for i, response in enumerate(responses): try: if isinstance(response, Exception): raise response json_data = await response.json() if not isinstance(json_data, dict): raise ValueError(f"Expected dict, got {type(json_data)}") results.append(json_data) except Exception as e: print(f"WARNING: Request {i} failed: {e}") results.append({"response": {"output": []}, "reward": 0.0, "error": str(e)}) return results def nemo_gym_rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]: is_eval = not trainer.model.training num_generations = ( trainer.args.num_generations_eval if is_eval and trainer.args.num_generations_eval else trainer.args.num_generations ) dataset = trainer.eval_dataset if is_eval and trainer.eval_dataset is not None else trainer.train_dataset expanded_prompts = [] expanded_dataset_items = [] for idx_str in prompts: idx = int(idx_str) item = json.loads(dataset[idx]["metadata"]) for _ in range(num_generations): expanded_prompts.append(idx_str) expanded_dataset_items.append(dict(item)) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: responses = loop.run_until_complete( call_nemo_gym_agents( expanded_prompts, expanded_dataset_items, trainer.args.agent_servers, trainer.args.request_timeout, trainer.args.max_completion_length, temperature=trainer.args.temperature, top_p=trainer.args.top_p, ) ) finally: loop.close() tokenizer = trainer.processing_class prompt_ids: list[list[int]] = [] completion_ids: list[list[int]] = [] # list of rollouts env_mask: list[list[int]] = [] # only train on assistant turns logprobs: list[list[float]] = [] env_rewards: list[float] = [] num_turns_list: list[int] = [] for i, response in enumerate(responses): eos_token_id = tokenizer.eos_token_id or 0 if not isinstance(response, dict) or response.get("error"): rollout_failed = True else: output_items = response.get("response", {}).get("output", []) has_content = output_items and any( item.get("type") == "function_call" or ( item.get("type") == "message" and any( c.get("type") == "output_text" and c.get("text", "").strip() for c in item.get("content", []) ) ) for item in output_items ) rollout_failed = not has_content if rollout_failed: prompt_ids.append([eos_token_id]) completion_ids.append([eos_token_id]) env_mask.append([0]) logprobs.append([0.0]) env_rewards.append(0.0) num_turns_list.append(0) continue episode_reward = response.get("reward", 0.0) output_items = response.get("response", {}).get("output", []) rollout_ids: list[int] = [] rollout_mask: list[int] = [] rollout_logprobs: list[float] = [] seen_token_ids: list[int] = [] first_prompt = None num_turns = 0 for _idx, item in enumerate(output_items): if "prompt_token_ids" not in item or "generation_token_ids" not in item: continue num_turns += 1 item_prompt_ids = item["prompt_token_ids"] item_gen_ids = item["generation_token_ids"] item_logprobs = item.get("generation_log_probs", []) tool_result_tokens = [] if first_prompt is None: first_prompt = item_prompt_ids seen_token_ids = list(item_prompt_ids) else: if len(item_prompt_ids) > len(seen_token_ids): if item_prompt_ids[: len(seen_token_ids)] != seen_token_ids: raise ValueError( f"[Turn {num_turns}] Non-contiguous messages (tokenization issue). " f"Expected prefix len {len(seen_token_ids)}, got prompt len {len(item_prompt_ids)}" ) tool_result_tokens = item_prompt_ids[len(seen_token_ids) :] if tool_result_tokens: rollout_ids.extend(tool_result_tokens) rollout_mask.extend([0] * len(tool_result_tokens)) rollout_logprobs.extend([0.0] * len(tool_result_tokens)) rollout_ids.extend(item_gen_ids) rollout_mask.extend([1] * len(item_gen_ids)) assert len(item_logprobs) == len(item_gen_ids), ( f"Logprobs len {len(item_logprobs)} != gen len {len(item_gen_ids)}" ) rollout_logprobs.extend(item_logprobs) seen_token_ids = list(item_prompt_ids) + list(item_gen_ids) if not rollout_ids or first_prompt is None: raise ValueError(f"Rollout {i} has no valid turns") prompt_ids.append(first_prompt) # list of prompts completion_ids.append(rollout_ids) # list of rollouts env_mask.append(rollout_mask) logprobs.append(rollout_logprobs) env_rewards.append(episode_reward) num_turns_list.append(num_turns) if not prompt_ids: raise RuntimeError("No valid rollouts. Check Nemo Gym and vLLM logs.") if num_turns_list: trainer.log( { "num_turns_mean": sum(num_turns_list) / len(num_turns_list), "num_turns_min": min(num_turns_list), "num_turns_max": max(num_turns_list), } ) unique_prompt_ids = prompt_ids[::num_generations] return { "prompt_ids": unique_prompt_ids, "completion_ids": completion_ids, "env_mask": env_mask, "logprobs": logprobs, "env_reward": env_rewards, "num_turns": num_turns_list, } def load_dataset_from_jsonl(path: str) -> Dataset: data = [] with open(path) as f: for idx, line in enumerate(f): if line.strip(): item = json.loads(line) data.append( { "prompt": str( idx ), # use index for lookup as not all nemo gym datasets have the same metadata fields. maybe not the most elegant "metadata": json.dumps(item), } ) return Dataset.from_list(data) def main(): parser = argparse.ArgumentParser(description="") parser.add_argument("--config", required=True, help="Path to config YAML file") parser.add_argument("--vllm_server_host", type=str, default="127.0.0.1", help="vLLM server hostname/IP") parser.add_argument("--head_server_host", type=str, default="127.0.0.1", help="Head server hostname/IP for ng_run") parser.add_argument("--resume_from_checkpoint", type=str, default=None, help="Path to checkpoint to resume from") args = parser.parse_args() with open(args.config) as f: config = yaml.safe_load(f) model_name = config.pop("model_name") dataset_path = config.pop("dataset_path") eval_dataset_path = config.pop("eval_dataset_path", None) task = config.pop("task", None) project_name = config.pop("project_name", None) if "learning_rate" in config and isinstance(config["learning_rate"], str): config["learning_rate"] = float(config["learning_rate"]) if "weight_decay" in config and isinstance(config["weight_decay"], str): config["weight_decay"] = float(config["weight_decay"]) agent_servers = get_agent_servers( head_server_host=args.head_server_host, head_server_port=11000, ) if project_name: os.environ["WANDB_PROJECT"] = project_name if dataset_path.endswith((".jsonl", ".json")): dataset = load_dataset_from_jsonl(dataset_path) else: dataset = load_dataset(dataset_path, split="train") eval_dataset = None if eval_dataset_path: eval_dataset = load_dataset_from_jsonl(eval_dataset_path) print(f"Eval dataset has {len(eval_dataset)} examples\n") training_args = NeMoGymGRPOConfig( use_vllm=True, vllm_mode="server", vllm_server_host=args.vllm_server_host, vllm_server_port=8000, gradient_checkpointing=True, num_generations_eval=1, logging_steps=1, epsilon=0.2, epsilon_high=0.28, loss_type="grpo", mask_truncated_completions=True, shuffle_dataset=False, model_init_kwargs={"torch_dtype": "auto"}, agent_servers=agent_servers, request_timeout=10800, **config, ) if training_args.run_name is None: task_name = task or os.path.basename(dataset_path).replace(".jsonl", "").replace(".json", "") model_short = model_name.split("/")[-1] training_args.run_name = ( f"{task_name}_{model_short}" f"_rpp{training_args.num_generations}" f"_dbs{training_args.per_device_train_batch_size}" f"_ga{training_args.gradient_accumulation_steps}" f"_maxlen{training_args.max_completion_length}" f"_lr{training_args.learning_rate}" f"_temp{training_args.temperature}" f"_topp{training_args.top_p}" ) tokenizer = AutoTokenizer.from_pretrained(model_name, truncation_side="left", padding_side="left") trainer = GRPOTrainer( model=model_name, processing_class=tokenizer, reward_funcs=reward_fn, train_dataset=dataset, eval_dataset=eval_dataset, rollout_func=nemo_gym_rollout_func, args=training_args, ) trainer.train(resume_from_checkpoint=args.resume_from_checkpoint) if __name__ == "__main__": main() ================================================ FILE: examples/scripts/online_dpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ Usage: python examples/scripts/online_dpo.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-1b-tldr-online-dpo \ --per_device_train_batch_size 8 \ --gradient_accumulation_steps 16 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 With LoRA: python examples/scripts/online_dpo.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-6 \ --output_dir pythia-1b-tldr-online-dpo \ --per_device_train_batch_size 16 \ --gradient_accumulation_steps 8 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --use_peft """ import os import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.experimental.judges import HfPairwiseJudge, OpenAIPairwiseJudge, PairRMJudge from trl.experimental.online_dpo import OnlineDPOConfig, OnlineDPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") JUDGES = {"pair_rm": PairRMJudge, "openai": OpenAIPairwiseJudge, "hf": HfPairwiseJudge} if __name__ == "__main__": parser = TrlParser((ScriptArguments, OnlineDPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.gradient_checkpointing_kwargs = {"use_reentrant": True} dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) if training_args.reward_model_path is not None: reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, num_labels=1, trust_remote_code=model_args.trust_remote_code, **model_kwargs, ) reward_tokenizer = AutoTokenizer.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, truncation=True, truncation_side="left", # since we judge the completion, truncating left is more appropriate ) if reward_tokenizer.pad_token_id is None: reward_tokenizer.pad_token = reward_tokenizer.eos_token else: reward_model = None reward_tokenizer = None if training_args.judge is not None: judge_cls = JUDGES[training_args.judge] judge = judge_cls() else: judge = None tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, padding_side="left", trust_remote_code=model_args.trust_remote_code, **model_kwargs, ) if tokenizer.pad_token_id is None: tokenizer.pad_token = tokenizer.eos_token dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) trainer = OnlineDPOTrainer( model=model, reward_funcs=reward_model, judge=judge, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, reward_processing_classes=reward_tokenizer, peft_config=get_peft_config(model_args), ) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_new_tokens, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/online_dpo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "math-verify", # "latex2sympy2_extended", # "trackio", # "torchvision", # "kernels", # ] # /// """ pip install math_verify # For Qwen/Qwen2.5-VL-3B-Instruct accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/online_dpo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --reward_model_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir online-dpo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_length 1536 \ --max_new_tokens 1024 \ --use_vllm \ --vllm_mode server \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 2 # For HuggingFaceTB/SmolVLM2-2.2B-Instruct pip install num2words==0.5.14 accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/online_dpo_vlm.py \ --model_name_or_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --reward_model_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --output_dir online-dpo-SmolVLM2-2.2B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_length 1536 \ --max_new_tokens 1024 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 2 # Single GPU test command: python examples/scripts/online_dpo_vlm.py \ --model_name_or_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --reward_model_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --output_dir online-dpo-SmolVLM2-2.2B-Instruct-test \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_length 1536 \ --max_new_tokens 128 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 1 \ --max_steps 2 \ --logging_steps 1 \ --trust_remote_code """ import os import torch import transformers from datasets import load_dataset from transformers import AutoConfig, AutoProcessor, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.experimental.online_dpo import OnlineDPOConfig, OnlineDPOTrainer from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, OnlineDPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.gradient_checkpointing_kwargs = {"use_reentrant": True} dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config # Load the VLM model using correct architecture (from GRPO pattern) config = AutoConfig.from_pretrained(model_args.model_name_or_path) architecture = getattr(transformers, config.architectures[0]) model = architecture.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) # For VLM online DPO, using a reward model is complex because it needs images # Instead, we'll use a simple random judge for testing # In production, you'd want to use a proper text-only reward model or a custom judge reward_model = None reward_processor = None # Load processor for main model processor = AutoProcessor.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, ) if hasattr(processor, "tokenizer"): processor.tokenizer.padding_side = "left" if processor.tokenizer.pad_token_id is None: processor.tokenizer.pad_token = processor.tokenizer.eos_token ################ # Dataset ################ dataset = load_dataset("lmms-lab/multimodal-open-r1-8k-verified", split="train") dataset = dataset.train_test_split(test_size=100, seed=42) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): # Create conversational format that OnlineDPOTrainer expects prompt = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ] return {"prompt": prompt, "image": example["image"]} dataset = dataset.map(make_conversation) # Filter big images (from GRPO pattern) def filter_big_images(example): image = example["image"] return image.size[0] < 512 and image.size[1] < 512 dataset = dataset.filter(filter_big_images) def convert_to_rgb(example): image = example["image"] if image.mode != "RGB": image = image.convert("RGB") example["image"] = image return example dataset = dataset.map(convert_to_rgb) train_dataset = dataset["train"] eval_dataset = dataset["test"] if training_args.eval_strategy != "no" else None ################ # Training ################ trainer = OnlineDPOTrainer( model=model, reward_funcs=[think_format_reward, accuracy_reward], # Use same reward functions as GRPO VLM args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processor, peft_config=get_peft_config(model_args), ) # Add completion logging callback (from online DPO pattern) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_new_tokens, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name="lmms-lab/multimodal-open-r1-8k-verified") ================================================ FILE: examples/scripts/openenv/browsergym.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "peft", # "trackio", # "kernels", # "openenv-browsergym @ git+https://huggingface.co/spaces/openenv/browsergym_env", # ] # /// """ Simple script to run GRPO training with OpenEnv's BrowserGym environment and vLLM. This example automatically detects and uses vision capabilities when VLM models are used. Screenshots from BrowserGym are collected and passed to the model during training. The GRPO trainer auto-detects multimodal support by checking for images in the rollout data. Setup (Option A - Install from HF Space, recommended): ```sh uv pip install git+https://huggingface.co/spaces/openenv/browsergym_env ``` Setup (Option B - Clone OpenEnv repo, for development): ```sh git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/browsergym_env uv pip install -e . ``` # Option 1: HF Spaces + Colocated vLLM (1 GPU required) ```sh python examples/scripts/openenv/browsergym.py --vllm-mode colocate ``` # Option 2: HF Spaces + Separate vLLM server (2 GPUs required) # Spin up vLLM server (Terminal 1) ```sh CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen3-VL-2B-Instruct --host 0.0.0.0 --port 8001 ``` # Run training (Terminal 2) ```sh CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/browsergym.py --vllm-mode server --vllm-server-url http://localhost:8001 ``` # Option 3: Local + Colocated vLLM (1 GPU required) # Build and start the environment only if using --env-mode docker-local ```sh cd OpenEnv docker build -t openenv-base:latest -f src/core/containers/images/Dockerfile . docker build -t browsergym-env:latest -f src/envs/browsergym_env/server/Dockerfile . docker run -d -p 8001:8001 \ -e BROWSERGYM_BENCHMARK="miniwob" \ -e BROWSERGYM_TASK_NAME="click-test" \ browsergym-env:latest ``` ```sh python examples/scripts/openenv/browsergym.py --env-mode docker-local --vllm-mode colocate ``` """ from __future__ import annotations import argparse from datetime import datetime from pathlib import Path import numpy as np from browsergym_env import BrowserGymAction, BrowserGymEnv from datasets import Dataset from PIL import Image from transformers import AutoTokenizer from trl import GRPOConfig, GRPOTrainer from trl.experimental.openenv import generate_rollout_completions def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run GRPO training for BrowserGym MiniWoB using OpenEnv environment.") parser.add_argument( "--tokenizer-id", default="Qwen/Qwen3-VL-2B-Instruct", help="Model identifier used to load the tokenizer.", ) parser.add_argument( "--model-id", default="Qwen/Qwen3-VL-2B-Instruct", help="Model identifier passed to GRPOTrainer for fine-tuning.", ) parser.add_argument( "--env-host", type=str, default="https://openenv-browsergym-env.hf.space", help="Host for the BrowserGym environment.", ) parser.add_argument("--env-port", type=int, default=8001, help="Port for the BrowserGym environment.") parser.add_argument( "--env-mode", choices=["docker-local", "docker-image", "docker-hub", "space"], default="space", help="Where to run the environment: 'local' to launch it, 'docker-local' if already running locally, 'docker-image' to run from a Docker image, 'docker-hub' to run from Docker Hub, or 'space' to use a remote Space URL.", ) parser.add_argument( "--env-image", type=str, default="browsergym-env:latest", help="Docker image for the BrowserGym environment." ) parser.add_argument( "--benchmark", default="miniwob", help="BrowserGym benchmark to use (miniwob, webarena, etc.).", ) parser.add_argument( "--task-name", default="click-test", help="Specific task within the benchmark (e.g., click-test, click-button).", ) parser.add_argument( "--dataset-prompt", default="Complete the web task successfully.", help="Prompt text used to seed the training dataset.", ) parser.add_argument( "--dataset-size", type=int, default=1000, help="Number of entries to include in the synthetic training dataset.", ) parser.add_argument( "--max-steps", type=int, default=10, help="Maximum number of steps per episode.", ) parser.add_argument( "--max-new-tokens", type=int, default=32, help="Maximum number of new tokens to request from vLLM for each action.", ) parser.add_argument( "--temperature", type=float, default=0.7, help="Sampling temperature used during rollout generation.", ) parser.add_argument( "--top-k", type=int, default=50, help="Top-k sampling parameter forwarded to vLLM.", ) parser.add_argument( "--top-p", type=float, default=None, help="Optional top-p sampling parameter forwarded to vLLM.", ) parser.add_argument( "--image-size", type=int, default=512, help="Resize screenshots to this size (preserving aspect ratio) to reduce memory usage. Set to 0 to disable resizing.", ) parser.add_argument( "--learning-rate", type=float, default=5e-6, help="Learning rate for GRPO training.", ) parser.add_argument( "--weight-decay", type=float, default=0.0, help="Weight decay applied during optimization.", ) parser.add_argument( "--gradient-accumulation-steps", type=int, default=32, help="Gradient accumulation steps for GRPO training.", ) parser.add_argument( "--warmup-steps", type=int, default=10, help="Warmup steps for the scheduler.", ) parser.add_argument( "--per-device-batch-size", type=int, default=1, help="Per-device train batch size.", ) parser.add_argument( "--num-generations", type=int, default=4, help="Number of rollout generations per dataset prompt.", ) parser.add_argument( "--num-epochs", type=int, default=1, help="Number of training epochs.", ) parser.add_argument( "--save-interval", type=int, default=50, help="Interval (in steps) between checkpoint saves.", ) parser.add_argument( "--save-total-limit", type=int, default=None, help="Maximum number of checkpoints to keep.", ) parser.add_argument( "--output-dir", default=None, help="Directory where training outputs and checkpoints are stored.", ) parser.add_argument( "--run-name", default=None, help="Optional run name for logging systems.", ) parser.add_argument( "--project", default=None, help="Optional project identifier for logging systems.", ) parser.add_argument( "--vllm-mode", choices=("colocate", "server"), default="colocate", help="vLLM execution mode: 'colocate' or 'server'.", ) parser.add_argument( "--vllm-server-url", type=str, default="http://localhost:8001", help="URL for the vLLM server (only used when --vllm-mode=server).", ) parser.add_argument( "--logging-steps", type=int, default=1, help="Frequency of logging steps for GRPO training.", ) parser.add_argument( "--debug", action="store_true", default=False, help="Enable verbose debugging output during rollouts.", ) return parser.parse_args() def sanitize_name(name: str) -> str: return name.replace("/", "-") # --------------------------------------------------------------------------- # System Prompt # --------------------------------------------------------------------------- SYSTEM_PROMPT = """You control a web browser through BrowserGym actions. You must complete the given web task by interacting with the page. Available actions: - noop() - Do nothing - click(bid) - Click element with BrowserGym ID - fill(bid, text) - Fill input field - send_keys(text) - Send keyboard input - scroll(direction) - Scroll up/down Reply with exactly ONE action on a single line, e.g.: click('123') fill('456', 'text') noop() Do not include explanations or multiple actions.""" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_user_prompt(goal: str, step_num: int, axtree: str, error: str = "") -> str: """Create user prompt from observation.""" prompt_parts = [f"Step {step_num + 1}"] if goal: prompt_parts.append(f"Goal: {goal}") if error: prompt_parts.append(f"Previous action error: {error}") # Include accessibility tree (truncated for context) if axtree: max_len = 2000 axtree_truncated = axtree[:max_len] + "..." if len(axtree) > max_len else axtree prompt_parts.append(f"Page structure:\n{axtree_truncated}") prompt_parts.append("What action do you take?") return "\n\n".join(prompt_parts) def parse_action(response_text: str) -> str: """Parse BrowserGym action from model response.""" # Extract first line that looks like an action for line in response_text.strip().split("\n"): line = line.strip() if "(" in line and ")" in line: return line # Fallback to noop if no valid action found return "noop()" def rollout_once( trainer: GRPOTrainer, env: BrowserGymEnv, tokenizer: AutoTokenizer, dataset_prompt: str, max_steps: int, image_size: int = 0, debug: bool = False, ) -> dict[str, list]: """Run one episode and collect training data.""" result = env.reset() observation = result.observation prompt_ids: list[int] = [] completion_ids: list[int] = [] logprobs: list[float] = [] step_rewards: list[float] = [] completion_rewards: list[float] = [] images: list[Image.Image] = [] # Collect screenshots for VLM for step_num in range(max_steps): if result.done: break # Create prompt from observation goal = observation.goal or dataset_prompt axtree = observation.axtree_txt or "" error = observation.error if observation.last_action_error else "" # Collect screenshot if available (for VLM support) if observation.screenshot is not None: screenshot_array = np.array(observation.screenshot, dtype=np.uint8) screenshot_image = Image.fromarray(screenshot_array) # Resize to reduce memory if image_size > 0 if image_size > 0: # Preserve aspect ratio while resizing screenshot_image.thumbnail((image_size, image_size), Image.LANCZOS) print( f"[DEBUG] Step {step_num + 1}: Collected and resized screenshot from {screenshot_array.shape} to {screenshot_image.size}" ) else: print(f"[DEBUG] Step {step_num + 1}: Collected screenshot, shape={screenshot_array.shape}") images.append(screenshot_image) else: print(f"[DEBUG] Step {step_num + 1}: No screenshot available") user_prompt = make_user_prompt(goal, step_num, axtree, error) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ] prompt_text = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=False, ) # Generate action with vLLM rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0] prompt_ids.extend(rollout_outputs["prompt_ids"]) completion_ids.extend(rollout_outputs["completion_ids"]) logprobs.extend(rollout_outputs["logprobs"]) completion_text = rollout_outputs.get("text") or tokenizer.decode( rollout_outputs["completion_ids"], skip_special_tokens=True ) # Parse and execute action action_str = parse_action(completion_text) if debug: print(f"Step {step_num + 1}: {action_str}") # Take action in environment result = env.step(BrowserGymAction(action_str=action_str)) observation = result.observation # Track rewards step_reward = float(result.reward or 0.0) step_rewards.append(step_reward) # Reward shaping: success is most important if result.done and step_reward > 0: completion_rewards.append(1.0) # Task completed successfully elif result.done and step_reward == 0: completion_rewards.append(0.0) # Task failed else: completion_rewards.append(step_reward) # Intermediate reward # Final reward is based on task completion final_reward = completion_rewards[-1] if completion_rewards else 0.0 result_dict = { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "step_rewards": step_rewards, "completion_reward": final_reward, } # Include images if available (GRPO trainer will auto-detect VLM support) if images: result_dict["images"] = images return result_dict # --------------------------------------------------------------------------- # Rewards # --------------------------------------------------------------------------- def reward_completion(completions: list[str], **kwargs) -> list[float]: """Reward for task completion.""" rewards = kwargs.get("completion_reward") if kwargs else None if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] # --------------------------------------------------------------------------- # Main entrypoint # --------------------------------------------------------------------------- def main() -> None: args = parse_args() tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_id) tokenizer.pad_token = tokenizer.eos_token # Select environment mode if args.env_mode == "docker-local": env_url = f"http://{args.env_host}:{args.env_port}" client = BrowserGymEnv(base_url=env_url) print(f"🌍 Using existing BrowserGym Environment (Docker) at: {env_url}") elif args.env_mode == "docker-image": client = BrowserGymEnv.from_docker_image(args.env_image) print("🌍 Using BrowserGym Environment (Docker) from local Image") elif args.env_mode == "docker-hub": client = BrowserGymEnv.from_hub(args.env_image) print("🌍 Using existing BrowserGym Environment (Docker) from Hub Image") elif args.env_mode == "space": env_url = args.env_host client = BrowserGymEnv(base_url=env_url) print(f"🌍 Using Hugging Face Space environment at: {env_url}") else: raise ValueError(f"Unknown environment mode: {args.env_mode}") dataset = Dataset.from_dict({"prompt": [args.dataset_prompt] * args.dataset_size}) timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") default_output_dir = Path("outputs") / f"browsergym-grpo-{sanitize_name(args.model_id)}-{timestamp}" output_dir = Path(args.output_dir or default_output_dir) grpo_config = GRPOConfig( use_vllm=True, vllm_mode=args.vllm_mode, vllm_server_base_url=args.vllm_server_url if args.vllm_mode == "server" else None, vllm_gpu_memory_utilization=0.4, output_dir=str(output_dir), num_train_epochs=args.num_epochs, learning_rate=args.learning_rate, weight_decay=args.weight_decay, gradient_accumulation_steps=args.gradient_accumulation_steps, per_device_train_batch_size=args.per_device_batch_size, warmup_steps=args.warmup_steps, num_generations=args.num_generations, generation_batch_size=args.num_generations, # Must be divisible by num_generations max_completion_length=args.max_new_tokens, logging_steps=args.logging_steps, report_to="trackio", trackio_space_id=f"browsergym-grpo-{sanitize_name(args.model_id)}-{timestamp}", save_strategy="steps", save_steps=args.save_interval, save_total_limit=args.save_total_limit, temperature=args.temperature, top_k=args.top_k, top_p=args.top_p, ) grpo_config.run_name = args.run_name or f"run-{timestamp}" grpo_config.project = args.project or f"group-{sanitize_name(args.model_id)}" def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]: episode_prompt_ids: list[list[int]] = [] episode_completion_ids: list[list[int]] = [] episode_logprobs: list[list[float]] = [] completion_rewards: list[float] = [] episode_images: list[list[Image.Image]] = [] print(f"\n[DEBUG] rollout_func called with {len(prompts)} prompts") for i, prompt_text in enumerate(prompts): print(f"[DEBUG] Processing prompt {i + 1}/{len(prompts)}") episode = rollout_once( trainer=trainer, env=client, tokenizer=tokenizer, dataset_prompt=prompt_text, max_steps=args.max_steps, image_size=args.image_size, debug=args.debug, ) episode_prompt_ids.append(episode["prompt_ids"]) episode_completion_ids.append(episode["completion_ids"]) episode_logprobs.append(episode["logprobs"]) completion_rewards.append(episode["completion_reward"]) # Collect images if available (for VLM support) if "images" in episode: print(f"[DEBUG] Episode {i + 1} has {len(episode['images'])} images") episode_images.append(episode["images"]) else: print(f"[DEBUG] Episode {i + 1} has NO images") result = { "prompt_ids": episode_prompt_ids, "completion_ids": episode_completion_ids, "logprobs": episode_logprobs, "completion_reward": completion_rewards, } # Include images if any episode had screenshots (GRPO trainer auto-detects VLM) if episode_images: result["images"] = episode_images print(f"[DEBUG] rollout_func returning with images: {len(episode_images)} episodes") else: print("[DEBUG] rollout_func returning WITHOUT images") return result trainer = GRPOTrainer( model=args.model_id, processing_class=tokenizer, reward_funcs=[reward_completion], train_dataset=dataset, args=grpo_config, rollout_func=rollout_func, ) print("=" * 80) print("Starting GRPO training with BrowserGym environment") print(f"Benchmark: {args.benchmark}") print(f"Task: {args.task_name}") print(f"Model: {args.model_id}") print(f"Using {args.num_generations} rollouts per dataset prompt") print(f"Output directory: {output_dir}") print("=" * 80) try: trainer.train() print("\nTraining completed successfully!") finally: client.close() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/openenv/browsergym_llm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "peft", # "trackio", # "kernels", # "openenv-browsergym @ git+https://huggingface.co/spaces/openenv/browsergym_env", # ] # /// """ Simple script to run GRPO training with OpenEnv's BrowserGym environment and vLLM for LLMs. This script is optimized for text-only Language Models (LLMs). It uses the accessibility tree text from BrowserGym, making it memory-efficient. The environment runs on a Hugging Face Space by default. Setup (Option A - Install from HF Space, recommended): ```sh uv pip install git+https://huggingface.co/spaces/openenv/browsergym_env ``` Setup (Option B - Clone OpenEnv repo, for development): ```sh git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/browsergym_env uv pip install -e . ``` # Option 1: HF Spaces + Colocated vLLM (1 GPU required) ```sh python examples/scripts/openenv/browsergym_llm.py --vllm-mode colocate ``` # Option 2: HF Spaces + Separate vLLM server (2 GPUs required) # Spin up vLLM server (Terminal 1) ```sh CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen3-0.6B --host 0.0.0.0 --port 8001 ``` # Run training (Terminal 2) ```sh CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/browsergym_llm.py --vllm-mode server --vllm-server-url http://localhost:8001 ``` """ from __future__ import annotations import argparse from datetime import datetime from pathlib import Path from browsergym_env import BrowserGymAction, BrowserGymEnv from datasets import Dataset from transformers import AutoTokenizer from trl import GRPOConfig, GRPOTrainer from trl.experimental.openenv import generate_rollout_completions def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Run GRPO training for BrowserGym MiniWoB using OpenEnv environment.") parser.add_argument( "--model-id", default="Qwen/Qwen3-0.6B", help="Model identifier passed to GRPOTrainer for fine-tuning.", ) parser.add_argument( "--space-url", type=str, default="https://openenv-browsergym-env.hf.space", help="URL for the Hugging Face Space running the BrowserGym environment.", ) parser.add_argument( "--benchmark", default="miniwob", help="BrowserGym benchmark to use (miniwob, webarena, etc.).", ) parser.add_argument( "--task-name", default="click-test", help="Specific task within the benchmark (e.g., click-test, click-button).", ) parser.add_argument( "--dataset-prompt", default="Complete the web task successfully.", help="Prompt text used to seed the training dataset.", ) parser.add_argument( "--dataset-size", type=int, default=1000, help="Number of entries to include in the synthetic training dataset.", ) parser.add_argument( "--max-steps", type=int, default=10, help="Maximum number of steps per episode.", ) parser.add_argument( "--max-new-tokens", type=int, default=32, help="Maximum number of new tokens to request from vLLM for each action.", ) parser.add_argument( "--temperature", type=float, default=0.7, help="Sampling temperature used during rollout generation.", ) parser.add_argument( "--top-k", type=int, default=50, help="Top-k sampling parameter forwarded to vLLM.", ) parser.add_argument( "--top-p", type=float, default=None, help="Optional top-p sampling parameter forwarded to vLLM.", ) parser.add_argument( "--learning-rate", type=float, default=5e-6, help="Learning rate for GRPO training.", ) parser.add_argument( "--weight-decay", type=float, default=0.0, help="Weight decay applied during optimization.", ) parser.add_argument( "--gradient-accumulation-steps", type=int, default=32, help="Gradient accumulation steps for GRPO training.", ) parser.add_argument( "--warmup-steps", type=int, default=10, help="Warmup steps for the scheduler.", ) parser.add_argument( "--per-device-batch-size", type=int, default=1, help="Per-device train batch size.", ) parser.add_argument( "--num-generations", type=int, default=4, help="Number of rollout generations per dataset prompt.", ) parser.add_argument( "--num-epochs", type=int, default=1, help="Number of training epochs.", ) parser.add_argument( "--save-interval", type=int, default=50, help="Interval (in steps) between checkpoint saves.", ) parser.add_argument( "--save-total-limit", type=int, default=None, help="Maximum number of checkpoints to keep.", ) parser.add_argument( "--output-dir", default=None, help="Directory where training outputs and checkpoints are stored.", ) parser.add_argument( "--run-name", default=None, help="Optional run name for logging systems.", ) parser.add_argument( "--project", default=None, help="Optional project identifier for logging systems.", ) parser.add_argument( "--vllm-mode", choices=("colocate", "server"), default="colocate", help="vLLM execution mode: 'colocate' or 'server'.", ) parser.add_argument( "--vllm-server-url", type=str, default="http://localhost:8001", help="URL for the vLLM server (only used when --vllm-mode=server).", ) parser.add_argument( "--logging-steps", type=int, default=1, help="Frequency of logging steps for GRPO training.", ) parser.add_argument( "--debug", action="store_true", default=False, help="Enable verbose debugging output during rollouts.", ) return parser.parse_args() def sanitize_name(name: str) -> str: return name.replace("/", "-") # --------------------------------------------------------------------------- # System Prompt # --------------------------------------------------------------------------- SYSTEM_PROMPT = """You control a web browser through BrowserGym actions. You must complete the given web task by interacting with the page. Available actions: - noop() - Do nothing - click(bid) - Click element with BrowserGym ID (the number in brackets) - fill(bid, text) - Fill input field with text - send_keys(text) - Send keyboard input - scroll(direction) - Scroll up/down The page structure shows elements as: [bid] element_type 'element_text' For example: [13] button 'Click Me!' means bid='13' Reply with exactly ONE action on a single line, e.g.: click('13') fill('42', 'hello world') noop() Do not include explanations or multiple actions.""" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_user_prompt(goal: str, step_num: int, axtree: str, error: str = "") -> str: """Create user prompt from observation.""" prompt_parts = [f"Step {step_num + 1}"] if goal: prompt_parts.append(f"Goal: {goal}") if error: prompt_parts.append(f"Previous action error: {error}") # Include accessibility tree (truncated for context) if axtree: max_len = 2000 axtree_truncated = axtree[:max_len] + "..." if len(axtree) > max_len else axtree prompt_parts.append(f"Page structure:\n{axtree_truncated}") prompt_parts.append("What action do you take?") return "\n\n".join(prompt_parts) def parse_action(response_text: str) -> str: """Parse BrowserGym action from model response.""" # Extract first line that looks like an action for line in response_text.strip().split("\n"): line = line.strip() if "(" in line and ")" in line: return line # Fallback to noop if no valid action found return "noop()" def rollout_once( trainer: GRPOTrainer, env: BrowserGymEnv, tokenizer: AutoTokenizer, dataset_prompt: str, max_steps: int, debug: bool = False, ) -> dict[str, list]: """Run one episode and collect training data (text-only, no screenshots).""" result = env.reset() observation = result.observation prompt_ids: list[int] = [] completion_ids: list[int] = [] logprobs: list[float] = [] step_rewards: list[float] = [] completion_rewards: list[float] = [] for step_num in range(max_steps): if result.done: break # Create prompt from observation (text-only using accessibility tree) goal = observation.goal or dataset_prompt axtree = observation.axtree_txt or "" error = observation.error if observation.last_action_error else "" user_prompt = make_user_prompt(goal, step_num, axtree, error) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ] prompt_text = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=False, ) # Generate action with vLLM rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0] prompt_ids.extend(rollout_outputs["prompt_ids"]) completion_ids.extend(rollout_outputs["completion_ids"]) logprobs.extend(rollout_outputs["logprobs"]) completion_text = rollout_outputs.get("text") or tokenizer.decode( rollout_outputs["completion_ids"], skip_special_tokens=True ) # Parse and execute action action_str = parse_action(completion_text) if debug: print(f"Step {step_num + 1}: {action_str}") # Take action in environment result = env.step(BrowserGymAction(action_str=action_str)) observation = result.observation # Track rewards step_reward = float(result.reward or 0.0) step_rewards.append(step_reward) # Reward shaping: success is most important if result.done and step_reward > 0: completion_rewards.append(1.0) # Task completed successfully elif result.done and step_reward == 0: completion_rewards.append(0.0) # Task failed else: completion_rewards.append(step_reward) # Intermediate reward # Final reward is based on task completion final_reward = completion_rewards[-1] if completion_rewards else 0.0 return { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "step_rewards": step_rewards, "completion_reward": final_reward, } # --------------------------------------------------------------------------- # Rewards # --------------------------------------------------------------------------- def reward_completion(completions: list[str], **kwargs) -> list[float]: """Reward for task completion.""" rewards = kwargs.get("completion_reward") if kwargs else None if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] # --------------------------------------------------------------------------- # Main entrypoint # --------------------------------------------------------------------------- def main() -> None: args = parse_args() # Connect to BrowserGym environment via Hugging Face Space client = BrowserGymEnv(base_url=args.space_url) print(f"🌍 Using Hugging Face Space environment at: {args.space_url}") dataset = Dataset.from_dict({"prompt": [args.dataset_prompt] * args.dataset_size}) timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") default_output_dir = Path("outputs") / f"browsergym-grpo-{sanitize_name(args.model_id)}-{timestamp}" output_dir = Path(args.output_dir or default_output_dir) grpo_config = GRPOConfig( use_vllm=True, vllm_mode=args.vllm_mode, vllm_server_base_url=args.vllm_server_url if args.vllm_mode == "server" else None, vllm_gpu_memory_utilization=0.4, output_dir=str(output_dir), num_train_epochs=args.num_epochs, learning_rate=args.learning_rate, weight_decay=args.weight_decay, gradient_accumulation_steps=args.gradient_accumulation_steps, per_device_train_batch_size=args.per_device_batch_size, warmup_steps=args.warmup_steps, num_generations=args.num_generations, generation_batch_size=args.num_generations, # Must be divisible by num_generations max_completion_length=args.max_new_tokens, logging_steps=args.logging_steps, report_to="trackio", trackio_space_id=f"browsergym-grpo-{sanitize_name(args.model_id)}-{timestamp}", save_strategy="steps", save_steps=args.save_interval, save_total_limit=args.save_total_limit, temperature=args.temperature, top_k=args.top_k, top_p=args.top_p, ) grpo_config.run_name = args.run_name or f"run-{timestamp}" grpo_config.project = args.project or f"group-{sanitize_name(args.model_id)}" def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]: episode_prompt_ids: list[list[int]] = [] episode_completion_ids: list[list[int]] = [] episode_logprobs: list[list[float]] = [] completion_rewards: list[float] = [] if args.debug: print(f"\n[DEBUG] rollout_func called with {len(prompts)} prompts (LLM mode, text-only)") for i, prompt_text in enumerate(prompts): if args.debug: print(f"[DEBUG] Processing prompt {i + 1}/{len(prompts)}") episode = rollout_once( trainer=trainer, env=client, tokenizer=trainer.processing_class, dataset_prompt=prompt_text, max_steps=args.max_steps, debug=args.debug, ) episode_prompt_ids.append(episode["prompt_ids"]) episode_completion_ids.append(episode["completion_ids"]) episode_logprobs.append(episode["logprobs"]) completion_rewards.append(episode["completion_reward"]) return { "prompt_ids": episode_prompt_ids, "completion_ids": episode_completion_ids, "logprobs": episode_logprobs, "completion_reward": completion_rewards, } trainer = GRPOTrainer( model=args.model_id, reward_funcs=[reward_completion], train_dataset=dataset, args=grpo_config, rollout_func=rollout_func, ) print("=" * 80) print("Starting GRPO training with BrowserGym environment (LLM mode)") print(f"Benchmark: {args.benchmark}") print(f"Task: {args.task_name}") print(f"Model: {args.model_id}") print("Mode: LLM (text-only, using accessibility tree)") print(f"Using {args.num_generations} rollouts per dataset prompt") print(f"Output directory: {output_dir}") print("=" * 80) try: trainer.train() print("\nTraining completed successfully!") finally: client.close() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/openenv/carla.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "openenv-carla-env @ git+https://huggingface.co/spaces/sergiopaniego/carla_env", # ] # /// """ Simple script to run GRPO training with OpenEnv's CARLA environment. The environment simulates an emergency driving scenario where pedestrians are ahead and the model must learn to observe the scene and take the correct action (e.g., swerve to an empty lane) to minimize casualties. Setup (Option A - Install from HF Space, recommended): ```sh uv pip install git+https://huggingface.co/spaces/sergiopaniego/carla_env ``` Setup (Option B - Clone OpenEnv repo, for development): ```sh git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/carla_env uv pip install -e . ``` Usage: ```sh python examples/scripts/openenv/carla.py python examples/scripts/openenv/carla.py --model Qwen/Qwen3-1.7B --env-urls https://server1.hf.space https://server2.hf.space ``` """ import argparse from carla_env import CarlaAction, CarlaEnv from datasets import Dataset from trl import GRPOConfig, GRPOTrainer def parse_args(): parser = argparse.ArgumentParser(description="Run GRPO training with CARLA environment.") parser.add_argument( "--model", type=str, default="Qwen/Qwen3-0.6B", help="Model to use for training.", ) parser.add_argument( "--env-urls", type=str, nargs="+", default=["https://sergiopaniego-carla-env.hf.space"], help="URLs for the CARLA environment servers (one per environment instance).", ) parser.add_argument( "--trackio-space-id", type=str, default="carla-grpo-trolley", help="Trackio space identifier.", ) parser.add_argument( "--hub-model-id", type=str, default=None, help="Hub model ID to push the trained model to (e.g., sergiopaniego/Qwen3-0.6B-carla-trolley-escape).", ) parser.add_argument( "--run-name", type=str, default=None, help="Run name for tracking.", ) return parser.parse_args() args = parse_args() _env_url_iter = iter(args.env_urls) # Each instance takes the next URL prompt = """You control an autonomous vehicle in an emergency. There are pedestrians ahead and you must \ decide what to do immediately. You have the following tools available: - `observe`: Advance time and get a new observation of the scene. - `emergency_stop`: Apply maximum braking to stop the vehicle. - `lane_change(direction)`: Change lane to the left or right. Direction must be "left" or "right". Observe the scene first, then decide the best course of action to minimize harm.""" dataset = Dataset.from_dict({"prompt": [[{"role": "user", "content": prompt}] for _ in range(1000)]}) SIM_TICKS = 10 # Number of simulation steps to advance after each action class CarlaGRPOEnv: def __init__(self): url = next(_env_url_iter) self.client = CarlaEnv(base_url=url, connect_timeout_s=30, message_timeout_s=120) @staticmethod def _describe(obs) -> str: """Build a text description from the observation fields.""" parts = [] parts.append(f"Speed: {obs.speed_kmh:.1f} km/h.") if obs.nearby_actors: for actor in obs.nearby_actors: parts.append(f"- {actor.get('type', 'actor')} at {actor.get('distance', '?')}m") else: parts.append("No nearby actors detected.") if obs.collision_detected: parts.append(f"COLLISION detected with {obs.collided_with or 'unknown'}!") return "\n".join(parts) def _advance(self, ticks: int = SIM_TICKS): """Advance the simulation by calling observe repeatedly, return the last result.""" result = None for _ in range(ticks): result = self.client.step(CarlaAction(action_type="observe")) if result.done: break return result def reset(self, **kwargs) -> str | None: result = self.client.reset(scenario_name="trolley_micro_escape_exists") self.reward = 0.0 return self._describe(result.observation) def observe(self) -> str: """ Get the current scene description without taking any action. Returns: The scene description with vehicle state and nearby actors. """ result = self._advance() self.reward = result.observation.rubric_reward or 0.0 return self._describe(result.observation) def emergency_stop(self) -> str: """ Apply maximum braking to stop the vehicle. Returns: The scene description after braking. """ self.client.step(CarlaAction(action_type="emergency_stop")) result = self._advance() self.reward = result.observation.rubric_reward or 0.0 return self._describe(result.observation) def lane_change(self, direction: str) -> str: """ Change lane to avoid obstacles. Args: direction: Direction to change lane, either "left" or "right". Returns: The scene description after changing lane. """ self.client.step(CarlaAction(action_type="lane_change", lane_direction=direction)) result = self._advance() self.reward = result.observation.rubric_reward or 0.0 return self._describe(result.observation) def reward_func(completions, environments, **kwargs): return [environment.reward for environment in environments] trainer = GRPOTrainer( model=args.model, train_dataset=dataset, reward_funcs=reward_func, args=GRPOConfig( chat_template_kwargs={"enable_thinking": False}, log_completions=True, logging_steps=2, num_completions_to_print=1, max_completion_length=1024, per_device_train_batch_size=len(args.env_urls), steps_per_generation=1, num_generations=len(args.env_urls), gradient_accumulation_steps=16, max_steps=50, push_to_hub=args.hub_model_id is not None, hub_model_id=args.hub_model_id, run_name=args.run_name, report_to="trackio", trackio_space_id=args.trackio_space_id, ), environment_factory=CarlaGRPOEnv, ) trainer.train() ================================================ FILE: examples/scripts/openenv/catch.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "peft", # "trackio", # "kernels", # "openenv-openspiel-env @ git+https://huggingface.co/spaces/openenv/openspiel_env", # ] # /// """ Simple script to run GRPO training with OpenEnv's Catch environment (OpenSpiel) and vLLM. The reward function is based on the catch game where the agent tries to catch falling balls. Setup (Option A - Install from HF Space, recommended): ```sh uv pip install git+https://huggingface.co/spaces/openenv/openspiel_env ``` Setup (Option B - Clone OpenEnv repo, for development): ```sh git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/openspiel_env uv pip install -e . ``` # Option 1: HF Spaces + Colocated vLLM (1 GPU required) ```sh python examples/scripts/openenv/catch.py --env-mode space --env-host https://openenv-openspiel-env.hf.space --vllm-mode colocate ``` # Option 2: HF Spaces + Separate vLLM server (2 GPUs required) # Spin up vLLM server (Terminal 1) ```sh CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen2.5-0.5B-Instruct --host 0.0.0.0 --port 8000 ``` # Run training (Terminal 2) ```sh CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/catch.py --env-mode space --env-host https://openenv-openspiel-env.hf.space --vllm-mode server --vllm-server-url http://localhost:8000 ``` # Option 3: Local + Colocated vLLM (1 GPU required) # Start the environment only if using --env-mode docker-local ```sh docker run -d -p 8001:8001 registry.hf.space/openenv-openspiel-env:latest ``` ```sh python examples/scripts/openenv/catch.py --env-mode docker-local --vllm-mode colocate ``` """ # ruff: noqa: T201 import argparse import os import re import subprocess import sys import time from pathlib import Path import requests from datasets import Dataset from openspiel_env import OpenSpielEnv from openspiel_env.models import OpenSpielAction from trl import GRPOConfig, GRPOTrainer, RichProgressCallback, apply_chat_template from trl.experimental.openenv import generate_rollout_completions def parse_args(): parser = argparse.ArgumentParser(description="Run GRPO training with OpenSpiel Catch environment and vLLM.") # --- Environment settings --- parser.add_argument("--env-host", type=str, default="0.0.0.0", help="Host for the environment server.") parser.add_argument("--env-port", type=int, default=8001, help="Port for the environment server.") parser.add_argument( "--env-mode", choices=["local", "docker-local", "docker-image", "docker-hub", "space"], default="docker-image", help="Where to run the environment: 'local' to launch it, 'docker-local' if already running locally, 'docker-image' to run from a Docker image, 'docker-hub' to run from Docker Hub, or 'space' to use a remote Space URL.", ) # --- Generation and model config --- parser.add_argument( "--model", type=str, default="Qwen/Qwen2.5-0.5B-Instruct", help="Model name or path.", ) parser.add_argument( "--dataset-size", type=int, default=1000, help="Number of prompts to use for training dataset.", ) parser.add_argument( "--env-image", type=str, default="openspiel-env:latest", help="Docker image for the OpenSpiel environment." ) parser.add_argument( "--vllm-mode", choices=["colocate", "server"], default="colocate", help="vLLM execution mode: 'colocate' or 'server'.", ) parser.add_argument( "--vllm-server-url", type=str, default="http://localhost:8000", help="URL for the vLLM server (only used when --vllm-mode=server).", ) return parser.parse_args() def start_env_server(env_host: str, env_port: int): """Launch the OpenSpiel Catch environment locally via uvicorn.""" env_url = f"http://{env_host}:{env_port}" print(f"⚡ Starting FastAPI server for OpenSpiel Catch Environment on {env_url}...") work_dir = str(Path.cwd().parent.absolute()) process = subprocess.Popen( [ sys.executable, "-m", "uvicorn", "envs.openspiel_env.server.app:app", "--host", env_host, "--port", str(env_port), ], env={**os.environ, "PYTHONPATH": f"{work_dir}/src"}, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=work_dir, ) print("⏳ Waiting for server to start...") time.sleep(5) try: requests.get(f"{env_url}/health", timeout=2) print("\n✅ OpenSpiel Catch Environment server is running!") except Exception as e: print(f"\n❌ Server failed to start: {e}") if process.stderr: print(process.stderr.read()) raise return process BASE_PROMPT = """You are an AI agent playing the game **Catch**. ### Game Description - The game is played on a **10×5 grid**. - There is one **falling ball** and one **paddle** that you control at the bottom. - The objective is to **move the paddle left or right to catch the ball** as it falls. - The episode ends when the ball reaches the bottom row: - You get **+1 reward** if you catch it. - You get **–1 reward** if you miss it. ### Observation Format Each observation is a flattened 10x5 grid (list of 50 floats). - 1.0 → occupied (ball or paddle) - 0.0 → empty cell ### Actions: - `0` → Move left - `1` → Stay - `2` → Move right Respond **only** with one integer: `0`, `1`, or `2`. ### Current Observation """ def reward_from_env(completions, **kwargs): rewards = kwargs.get("env_reward", []) return [float(r) for r in rewards] if rewards else [0.0] * len(completions) def main(): args = parse_args() # Select environment mode if args.env_mode == "local": env_url = f"http://{args.env_host}:{args.env_port}" server_process = start_env_server(args.env_host, args.env_port) elif args.env_mode == "docker-local": env_url = f"http://{args.env_host}:{args.env_port}" server_process = None print(f"🌍 Using existing OpenSpiel Environment (Docker) at: {env_url}") elif args.env_mode == "docker-image": client = OpenSpielEnv.from_docker_image(args.env_image) server_process = None print("🌍 Using OpenSpiel Environment (Docker) from local Image") elif args.env_mode == "docker-hub": client = OpenSpielEnv.from_hub(args.env_image) server_process = None print("🌍 Using existing OpenSpiel Environment (Docker) from Hub Image") elif args.env_mode == "space": env_url = args.env_host server_process = None print(f"🌍 Using Hugging Face Space environment at: {env_url}") else: raise ValueError(f"Unknown environment mode: {args.env_mode}") if args.env_mode != "docker-hub" and args.env_mode != "docker-image": client = OpenSpielEnv(base_url=env_url) dataset = Dataset.from_dict({"prompt": [BASE_PROMPT] * args.dataset_size}) training_args = GRPOConfig( output_dir=f"{args.model.split('/')[-1]}-GRPO-Catch", use_vllm=True, vllm_mode=args.vllm_mode, vllm_server_base_url=args.vllm_server_url if args.vllm_mode == "server" else None, logging_steps=1, report_to="trackio", trackio_space_id=f"{args.model.split('/')[-1]}-GRPO-Catch", num_train_epochs=1, max_completion_length=4, gradient_accumulation_steps=4, ) def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]: """Generate completions via vLLM (colocated or server) and compute environment rewards.""" env_rewards: list[float] = [] all_prompt_ids: list[list[int]] = [] all_completion_ids: list[list[int]] = [] all_logprobs: list[list[float]] = [] tokenizer = trainer.processing_class for base_prompt in prompts: env_result = client.reset() obs = env_result.observation total_reward = 0.0 episode_prompt_ids: list[int] = [] episode_completion_ids: list[int] = [] episode_logprobs: list[float] = [] while not obs.done: episode_msg = {"prompt": [{"role": "user", "content": f"{base_prompt}\n\n{obs.info_state}\n"}]} episode_prompt = apply_chat_template(episode_msg, tokenizer) rollout_output = generate_rollout_completions(trainer, [episode_prompt["prompt"]])[0] episode_prompt_ids.extend(rollout_output["prompt_ids"]) episode_completion_ids.extend(rollout_output["completion_ids"]) episode_logprobs.extend(rollout_output["logprobs"]) completion_text = tokenizer.batch_decode([rollout_output["completion_ids"]], skip_special_tokens=True)[ 0 ] numbers = re.findall(r"\b([0-2])\b", completion_text) action_id = int(numbers[0]) if numbers else obs.legal_actions[0] env_result = client.step(OpenSpielAction(action_id=action_id, game_name="catch")) total_reward += env_result.reward or 0.0 obs = env_result.observation env_rewards.append(total_reward) all_prompt_ids.append(episode_prompt_ids) all_completion_ids.append(episode_completion_ids) all_logprobs.append(episode_logprobs) return { "prompt_ids": all_prompt_ids, "completion_ids": all_completion_ids, "logprobs": all_logprobs, "env_reward": env_rewards, } trainer = GRPOTrainer( model=args.model, reward_funcs=reward_from_env, args=training_args, train_dataset=dataset, rollout_func=rollout_func, callbacks=[RichProgressCallback()], ) trainer.train() time.sleep(5) if server_process: print("🛑 Terminating environment server...") server_process.terminate() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/openenv/echo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "openenv-echo-env @ git+https://huggingface.co/spaces/qgallouedec/echo_env", # ] # /// from datasets import Dataset from echo_env import EchoEnv from echo_env.models import EchoAction from trl import GRPOConfig, GRPOTrainer dataset = Dataset.from_dict( { "prompt": [ [{"role": "user", "content": "Try to echo 'Hello World!' in the environment."}], [{"role": "user", "content": "Make the environment echo 'Goodbye World!'"}], [{"role": "user", "content": "Can you ask the environment to echo 'TRL is great!'?"}], [{"role": "user", "content": "What happens if you ask the environment to echo 'I love RLHF!'?"}], [{"role": "user", "content": "Try to make the environment echo 'OpenEnv is awesome!'"}], ], } ) def reward_func(completions, environments, **kwargs): return [environment.get_reward() for environment in environments] class MyEchoEnv: def __init__(self): self.env = EchoEnv(base_url="https://qgallouedec-echo-env.hf.space") def reset(self, **kwargs) -> None | str: self._reward = None return None def step(self, message: str) -> str: """ Echo the message back from the environment. Args: message: The message to echo Returns: The echoed message. """ observation = self.env.step(EchoAction(message=message)) self._reward = observation.observation.reward return observation.observation.echoed_message def get_reward(self) -> float: """ Get the reward from the last step. Returns: The reward value. """ return self._reward trainer = GRPOTrainer( model="Qwen/Qwen3-0.6B", train_dataset=dataset, reward_funcs=reward_func, args=GRPOConfig( chat_template_kwargs={"enable_thinking": False}, log_completions=True, logging_steps=2, num_completions_to_print=1, ), environment_factory=MyEchoEnv, ) trainer.train() ================================================ FILE: examples/scripts/openenv/sudoku.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "peft", # "trackio", # "kernels", # "openenv-textarena @ git+https://huggingface.co/spaces/openenv/sudoku", # ] # /// """ GRPO training for Sudoku with TextArena environment. Setup (Option A - Install from HF Space, recommended): ```sh uv pip install git+https://huggingface.co/spaces/openenv/sudoku ``` Setup (Option B - Clone OpenEnv repo, for development): ```sh git clone https://github.com/meta-pytorch/OpenEnv.git cd OpenEnv/envs/textarena_env uv pip install -e . ``` # Option 1: HF Spaces + Colocated vLLM (1 GPU required) ```sh python examples/scripts/openenv/sudoku.py --vllm-mode colocate ``` # Option 2: HF Spaces + Separate vLLM server (2 GPUs required) # Spin up vLLM server (Terminal 1) ```sh CUDA_VISIBLE_DEVICES=0 trl vllm-serve --model Qwen/Qwen3-1.7B --host 0.0.0.0 --port 8000 ``` # Run training (Terminal 2) ```sh CUDA_VISIBLE_DEVICES=1 python examples/scripts/openenv/sudoku.py --vllm-mode server --vllm-server-url http://localhost:8000 ``` # Option 3: Local + Colocated vLLM (1 GPU required) # Start the environment only if using --env-mode docker-local ```sh docker run -d -p 8001:8001 registry.hf.space/openenv-sudoku:latest ``` ```sh python examples/scripts/openenv/sudoku.py --env-mode docker-local --vllm-mode colocate ``` # Full example with all flags: ```sh python examples/scripts/openenv/sudoku.py \ --vllm-mode colocate \ --env-mode space \ --env-host https://openenv-sudoku.hf.space \ --num-generations 8 \ --per-device-batch-size 1 \ --max-turns 100 \ --gradient-accumulation-steps 8 \ --difficulty easy \ --dataset-size 100 ``` """ from __future__ import annotations import argparse import re import sys import time from collections import defaultdict from datetime import datetime from pathlib import Path from datasets import Dataset from transformers import AutoTokenizer from trl import GRPOConfig, GRPOTrainer from trl.experimental.openenv import generate_rollout_completions # Ensure src/ is on the path sys.path.insert(0, str(Path(__file__).parent / "src")) from textarena_env import TextArenaAction, TextArenaEnv # --------------------------------------------------------------------------- # Argument parsing # --------------------------------------------------------------------------- def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="GRPO training for Sudoku") # Model parser.add_argument("--model-id", default="Qwen/Qwen3-1.7B") # Environment parser.add_argument("--env-host", type=str, default="https://openenv-sudoku.hf.space") parser.add_argument("--env-port", type=int, default=8001) parser.add_argument("--env-mode", choices=["docker-local", "docker-image", "docker-hub", "space"], default="space") parser.add_argument("--env-image", type=str, default="textarena-env:latest") # Prompts parser.add_argument("--system-prompt-path", default="sudoku_prompt.txt") parser.add_argument("--dataset-prompt", default="Play Sudoku like an expert.") parser.add_argument("--dataset-size", type=int, default=1000) # Game settings parser.add_argument("--max-turns", type=int, default=100) parser.add_argument("--max-new-tokens", type=int, default=8) parser.add_argument( "--difficulty", type=str, choices=["easy", "medium", "hard"], default="hard", help="Training difficulty: easy=guaranteed+options, medium=only options, hard=no hints", ) parser.add_argument( "--api-delay", type=float, default=0.0, help="Delay in seconds between API calls to avoid rate limiting" ) # Sampling parser.add_argument("--temperature", type=float, default=0.8) parser.add_argument("--top-k", type=int, default=10) parser.add_argument("--top-p", type=float, default=None) # Training parser.add_argument("--learning-rate", type=float, default=5e-6) parser.add_argument("--weight-decay", type=float, default=0.0) parser.add_argument("--gradient-accumulation-steps", type=int, default=64) parser.add_argument("--warmup-steps", type=int, default=20) parser.add_argument("--per-device-batch-size", type=int, default=1) parser.add_argument("--num-generations", type=int, default=2) parser.add_argument("--num-epochs", type=int, default=1) # Checkpoints parser.add_argument("--save-interval", type=int, default=10) parser.add_argument("--save-total-limit", type=int, default=None) parser.add_argument("--output-dir", default=None) # Logging parser.add_argument("--run-name", default=None) parser.add_argument("--project", default=None) parser.add_argument("--trackio-space-id", default="Sudoku-GRPO") parser.add_argument("--logging-steps", type=int, default=1) parser.add_argument("--debug", action="store_true", default=False) parser.add_argument( "--gradient-checkpointing", action="store_true", default=True, help="Enable gradient checkpointing to save memory", ) # vLLM parser.add_argument("--vllm-mode", choices=("colocate", "server"), default="colocate") parser.add_argument("--vllm-server-url", type=str, default="http://localhost:8000") parser.add_argument("--vllm-gpu-memory-utilization", type=float, default=0.2) return parser.parse_args() # --------------------------------------------------------------------------- # Helper functions # --------------------------------------------------------------------------- def resolve_system_prompt(path: str) -> str: prompt_path = Path(path) if not prompt_path.is_file(): prompt_path = Path(__file__).parent / path return prompt_path.read_text() def sanitize_name(name: str) -> str: return name.replace("/", "-") def extract_sudoku_move(text: str) -> str: """Extract a Sudoku move [row col number] from text.""" # Try with spaces match = re.search(r"\[(\d)\s+(\d)\s+(\d)\]", text) if match: row, col, num = match.groups() return f"[{row} {col} {num}]" # Try without spaces match = re.search(r"\[(\d)(\d)(\d)\]", text) if match: row, col, num = match.groups() return f"[{row} {col} {num}]" return "" def is_valid_board_state(board_str: str) -> bool: """Check if the string contains an actual Sudoku board.""" return "R1" in board_str and "R9" in board_str and "|" in board_str def parse_board(board_str: str) -> list[list[int]]: """Parse board string into 9x9 grid (0 = empty).""" grid = [[0] * 9 for _ in range(9)] if not is_valid_board_state(board_str): return grid for line in board_str.split("\n"): line_stripped = line.strip() if line_stripped and line_stripped[0] == "R" and len(line_stripped) > 1 and line_stripped[1].isdigit(): row = int(line_stripped[1]) - 1 # 0-indexed cell_part = line_stripped[2:] col = 0 for char in cell_part: if char == ".": grid[row][col] = 0 col += 1 elif char.isdigit(): grid[row][col] = int(char) col += 1 return grid def count_filled_cells(board_str: str) -> int: """Count the number of filled cells in the board.""" if not is_valid_board_state(board_str): return 0 grid = parse_board(board_str) return sum(1 for row in grid for cell in row if cell != 0) def get_valid_numbers(grid: list[list[int]], row: int, col: int) -> set[int]: """Get valid numbers for a cell based on Sudoku rules.""" if grid[row][col] != 0: return set() used = set() # Check row for c in range(9): if grid[row][c] != 0: used.add(grid[row][c]) # Check column for r in range(9): if grid[r][col] != 0: used.add(grid[r][col]) # Check 3x3 box box_row, box_col = 3 * (row // 3), 3 * (col // 3) for r in range(box_row, box_row + 3): for c in range(box_col, box_col + 3): if grid[r][c] != 0: used.add(grid[r][c]) return set(range(1, 10)) - used def extract_empty_cells_with_candidates( board_str: str, sort_by_difficulty: bool = True ) -> list[tuple[int, int, set[int]]]: """Extract empty cells with their valid candidate numbers. Args: sort_by_difficulty: If True, sort by number of candidates (easiest first). If False, keep natural order (top-left to bottom-right). """ grid = parse_board(board_str) cells_with_candidates = [] for row in range(9): for col in range(9): if grid[row][col] == 0: candidates = get_valid_numbers(grid, row, col) cells_with_candidates.append((row + 1, col + 1, candidates)) # 1-indexed if sort_by_difficulty: # Sort by number of candidates (easiest first = naked singles) cells_with_candidates.sort(key=lambda x: len(x[2])) return cells_with_candidates def extract_empty_cells(board_str: str) -> list[tuple[int, int]]: """Extract list of empty cells (row, col) from board string.""" empty_cells = [] if not is_valid_board_state(board_str): return empty_cells for line in board_str.split("\n"): line_stripped = line.strip() if line_stripped and line_stripped[0] == "R" and len(line_stripped) > 1 and line_stripped[1].isdigit(): row = int(line_stripped[1]) cell_part = line_stripped[2:] col = 0 for char in cell_part: if char == ".": col += 1 empty_cells.append((row, col)) elif char.isdigit(): col += 1 return empty_cells def extract_board_only(text: str) -> str: """Extract just the Sudoku grid from a message.""" if not text: return "" lines = text.split("\n") board_lines = [] in_board = False for line in lines: stripped = line.strip() if stripped.startswith("C1") or ( stripped and stripped[0] == "R" and len(stripped) > 1 and stripped[1].isdigit() ): in_board = True if in_board and (stripped.startswith("-") or stripped.startswith("R") or stripped.startswith("C1")): board_lines.append(line) elif ( in_board and stripped and not stripped.startswith("-") and not (stripped[0] == "R" and len(stripped) > 1 and stripped[1].isdigit()) ): break return "\n".join(board_lines) if board_lines else "" def make_compact_prompt( board: str, step: int, successful_moves: list[str], failed_moves: list[str], difficulty: str = "hard", ) -> str: """Create a compact prompt with only essential info (saves tokens!). Args: difficulty: Training difficulty level: - "easy": Show guaranteed moves (naked singles) + other options - "medium": Only show other options (hints where to look, not exact answers) - "hard": No hints (model must learn Sudoku rules by itself) """ # Summary line cells_filled = len(successful_moves) summary = f"Step {step}. Progress: {cells_filled} cells filled." # Board (only show the grid, stripped down) board_only = extract_board_only(board) if board else "No board available." # Moves already tried (for learning what NOT to do) tried_moves_hint = "" all_tried = successful_moves + failed_moves if all_tried: tried_moves_hint = f"\n\n⚠️ MOVES ALREADY TRIED (do not repeat): {', '.join(all_tried)}" # Hints based on difficulty hints = "" if difficulty == "easy" and board: # Easy: sorted by difficulty, show guaranteed moves + other easy options cells_with_candidates = extract_empty_cells_with_candidates(board, sort_by_difficulty=True) if cells_with_candidates: guaranteed = [] other_hints = [] for row, col, candidates in cells_with_candidates[:10]: if len(candidates) == 1: num = list(candidates)[0] guaranteed.append(f"[{row} {col} {num}]") elif len(candidates) <= 3: nums = ",".join(str(n) for n in sorted(candidates)) other_hints.append(f"({row},{col})→{nums}") if guaranteed: hints = f"\n\n🎯 GUARANTEED MOVES: {', '.join(guaranteed[:5])}" if other_hints: hints += f"\nOther options: {' | '.join(other_hints[:5])}" elif difficulty == "medium" and board: # Medium: NOT sorted, just show empty cells with candidates (no ordering hints) cells_with_candidates = extract_empty_cells_with_candidates(board, sort_by_difficulty=False) if cells_with_candidates: cell_hints = [] for row, col, candidates in cells_with_candidates[:10]: nums = ",".join(str(n) for n in sorted(candidates)) cell_hints.append(f"({row},{col})→{nums}") if cell_hints: hints = f"\n\nEmpty cells: {' | '.join(cell_hints)}" return f"{summary}\n\nBoard:\n{board_only}{tried_moves_hint}{hints}\n\nYour move:" def check_move_targets_empty_cell(move: str, board_str: str) -> bool: """Check if the move targets an empty cell on the board.""" if not move or not board_str: return False match = re.search(r"\[(\d)\s+(\d)\s+(\d)\]", move) if not match: return False row, col = int(match.group(1)), int(match.group(2)) empty_cells = extract_empty_cells(board_str) return (row, col) in empty_cells def extract_feedback(observation) -> dict: """Extract feedback from environment observation.""" feedback = {"valid_move": True, "got_warning": False, "board_state": ""} if not observation or not observation.messages: return feedback for message in observation.messages: content = message.content.lower() if message.content else "" if any(kw in content for kw in ["invalid", "error", "cannot", "already", "violation", "lost"]): feedback["valid_move"] = False if "please resubmit" in content or "avoid penalties" in content: feedback["got_warning"] = True if message.content and "|" in message.content and "R1" in message.content: feedback["board_state"] = message.content return feedback # --------------------------------------------------------------------------- # Rollout # --------------------------------------------------------------------------- def rollout_once( trainer: GRPOTrainer, env: TextArenaEnv, tokenizer: AutoTokenizer, system_prompt: str, max_turns: int, debug: bool = False, difficulty: str = "hard", api_delay: float = 0.0, ) -> dict[str, list]: result = env.reset() time.sleep(api_delay) # Avoid rate limiting observation = result.observation # Only store the LAST turn for backprop (much more efficient!) last_turn_data: dict | None = None valid_move_scores: list[float] = [] empty_cell_scores: list[float] = [] correct_scores: list[float] = [] repetition_scores: list[float] = [] move_counts: defaultdict[str, int] = defaultdict(int) # Track successful and failed moves for summary successful_moves: list[str] = [] failed_moves: list[str] = [] # Extract initial board state last_board_state = "" initial_filled = 0 for message in observation.messages: if message.content and is_valid_board_state(message.content): last_board_state = message.content initial_filled = count_filled_cells(last_board_state) break max_filled = initial_filled # Track max progress for turn in range(max_turns): if result.done: break # Build COMPACT prompt (saves tokens!) user_prompt = make_compact_prompt( board=last_board_state, step=turn + 1, successful_moves=successful_moves, failed_moves=failed_moves, difficulty=difficulty, ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ] prompt_text = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=False, enable_thinking=False ) if debug: print(f"\n{'=' * 60}") print(f"STEP {turn + 1}") print(f"{'=' * 60}") print(f"USER PROMPT:\n{user_prompt}") print(f"{'=' * 60}") # Generate rollout_outputs = generate_rollout_completions(trainer, [prompt_text])[0] # Store ONLY this turn's data (replace previous) last_turn_data = { "prompt_ids": rollout_outputs["prompt_ids"], "completion_ids": rollout_outputs["completion_ids"], "logprobs": rollout_outputs["logprobs"], } if debug: step_tokens = len(rollout_outputs["prompt_ids"]) + len(rollout_outputs["completion_ids"]) print(f"TOKENS: this_step={step_tokens} (only last turn used for backprop)") completion_text = rollout_outputs.get("text") or tokenizer.decode( rollout_outputs["completion_ids"], skip_special_tokens=True ) # Extract move move = extract_sudoku_move(completion_text) if debug: print(f"MODEL OUTPUT: {completion_text}") print(f"EXTRACTED MOVE: {move}") # Step environment result = env.step(TextArenaAction(message=move)) time.sleep(api_delay) # Avoid rate limiting observation = result.observation correct_score = float(result.reward or 0.0) # Get feedback feedback = extract_feedback(observation) # Get environment response env_response = "" for msg in observation.messages: if msg.sender_id == -1: # Environment message env_response = msg.content break if debug: print( f"ENV RESPONSE: {env_response[:200]}..." if len(env_response) > 200 else f"ENV RESPONSE: {env_response}" ) print(f"VALID: {feedback['valid_move']}, WARNING: {feedback['got_warning']}, REWARD: {correct_score}") # Calculate empty_cell_score if last_board_state and move: targets_empty = check_move_targets_empty_cell(move, last_board_state) empty_cell_score = 1.0 if targets_empty else -1.0 else: empty_cell_score = 0.0 # Calculate valid_move_score and repetition_score is_new_move = move_counts[move] == 0 repetition_count = move_counts[move] move_counts[move] += 1 # Exponential penalty for repetitions: -2^(n-1) capped at -10 # 1st repeat: -1, 2nd: -2, 3rd: -4, 4th+: -10 (capped) if repetition_count > 0: repetition_score = -min(2 ** (repetition_count - 1), 10.0) else: repetition_score = 0.0 if debug: print( f"SCORES: empty_cell={empty_cell_score}, is_new={is_new_move}, repetitions={repetition_count}, rep_penalty={repetition_score}" ) if not debug: print(f"Step {turn + 1}: {move}") if feedback["valid_move"] and is_new_move: valid_move_score = 1.0 if move: successful_moves.append(move) # Track for summary elif feedback["got_warning"]: valid_move_score = -0.5 if move: failed_moves.append(move) # Track for summary else: valid_move_score = 0.0 # Update board state and track progress if feedback["board_state"] and is_valid_board_state(feedback["board_state"]): last_board_state = feedback["board_state"] current_filled = count_filled_cells(last_board_state) if current_filled > max_filled: max_filled = current_filled valid_move_scores.append(valid_move_score) empty_cell_scores.append(empty_cell_score) correct_scores.append(correct_score) repetition_scores.append(repetition_score) # Aggregate rewards correct_reward = correct_scores[-1] if correct_scores else 0.0 valid_move_reward = sum(valid_move_scores) / len(valid_move_scores) if valid_move_scores else 0.0 empty_cell_reward = sum(empty_cell_scores) / len(empty_cell_scores) if empty_cell_scores else 0.0 repetition_reward = sum(repetition_scores) / len(repetition_scores) if repetition_scores else 0.0 # Progress reward: how many cells we filled beyond initial state (normalized to 0-1) # 81 total cells, so (max_filled - initial_filled) / (81 - initial_filled) gives progress remaining_to_fill = 81 - initial_filled if remaining_to_fill > 0: progress_reward = (max_filled - initial_filled) / remaining_to_fill else: progress_reward = 1.0 # Already complete # Use ONLY last turn for backpropagation (much more efficient!) if last_turn_data: prompt_ids = last_turn_data["prompt_ids"] completion_ids = last_turn_data["completion_ids"] logprobs = last_turn_data["logprobs"] else: prompt_ids = [] completion_ids = [] logprobs = [] total_tokens = len(prompt_ids) + len(completion_ids) cells_filled = max_filled - initial_filled print( f"Episode: empty_cell={empty_cell_reward:.2f}, valid={valid_move_reward:.2f}, " f"repetition={repetition_reward:.2f}, progress={progress_reward:.2f} ({cells_filled} cells), " f"correct={correct_reward:.2f}, tokens={total_tokens}" ) return { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "correct_reward": correct_reward, "valid_move_reward": valid_move_reward, "empty_cell_reward": empty_cell_reward, "repetition_reward": repetition_reward, "progress_reward": progress_reward, } # --------------------------------------------------------------------------- # Reward functions # --------------------------------------------------------------------------- def reward_empty_cell(completions: list[str], **kwargs) -> list[float]: """Reward for targeting empty cells (learn to pick valid positions first).""" rewards = kwargs.get("empty_cell_reward") if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] def reward_valid_moves(completions: list[str], **kwargs) -> list[float]: """Reward for making valid moves.""" rewards = kwargs.get("valid_move_reward") if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] def reward_correct(completions: list[str], **kwargs) -> list[float]: """Reward for solving the puzzle.""" rewards = kwargs.get("correct_reward") if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] def reward_repetition(completions: list[str], **kwargs) -> list[float]: """Penalty for repeating moves.""" rewards = kwargs.get("repetition_reward") if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] def reward_progress(completions: list[str], **kwargs) -> list[float]: """Reward for filling more cells in the board.""" rewards = kwargs.get("progress_reward") if rewards is None: return [0.0 for _ in completions] return [float(r) for r in rewards] # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main() -> None: args = parse_args() # Setup environment if args.env_mode == "docker-local": client = TextArenaEnv(base_url=f"http://{args.env_host}:{args.env_port}") elif args.env_mode == "docker-image": client = TextArenaEnv.from_docker_image(args.env_image) elif args.env_mode == "docker-hub": client = TextArenaEnv.from_hub(args.env_image) elif args.env_mode == "space": client = TextArenaEnv(base_url=args.env_host) else: raise ValueError(f"Unknown environment mode: {args.env_mode}") print(f"🌍 Environment: {args.env_mode}") system_prompt = resolve_system_prompt(args.system_prompt_path) dataset = Dataset.from_dict({"prompt": [args.dataset_prompt] * args.dataset_size}) timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") output_dir = Path(args.output_dir or f"outputs/sudoku-grpo-{sanitize_name(args.model_id)}-{timestamp}") grpo_config = GRPOConfig( use_vllm=True, vllm_mode=args.vllm_mode, vllm_server_base_url=args.vllm_server_url if args.vllm_mode == "server" else None, vllm_gpu_memory_utilization=args.vllm_gpu_memory_utilization if args.vllm_gpu_memory_utilization else 0.2, # Lower to leave more VRAM for backpropagation output_dir=str(output_dir), num_train_epochs=args.num_epochs, learning_rate=args.learning_rate, weight_decay=args.weight_decay, gradient_accumulation_steps=args.gradient_accumulation_steps, per_device_train_batch_size=args.per_device_batch_size, warmup_steps=args.warmup_steps, num_generations=args.num_generations, max_completion_length=args.max_new_tokens, logging_steps=args.logging_steps, save_strategy="steps", save_steps=args.save_interval, save_total_limit=args.save_total_limit, temperature=args.temperature, top_k=args.top_k, top_p=args.top_p, report_to="trackio", # chat_template_kwargs={"enable_thinking": False}, ) grpo_config.run_name = args.run_name or f"run-{timestamp}" grpo_config.project = args.project or f"group-{sanitize_name(args.model_id)}" grpo_config.trackio_space_id = args.trackio_space_id grpo_config.gradient_checkpointing = args.gradient_checkpointing def rollout_func(prompts: list[str], trainer: GRPOTrainer) -> dict[str, list]: all_prompt_ids = [] all_completion_ids = [] all_logprobs = [] all_correct = [] all_valid = [] all_empty_cell = [] all_repetition = [] all_progress = [] for _ in prompts: episode = rollout_once( trainer=trainer, env=client, tokenizer=trainer.processing_class, system_prompt=system_prompt, max_turns=args.max_turns, debug=args.debug, difficulty=args.difficulty, api_delay=args.api_delay, ) all_prompt_ids.append(episode["prompt_ids"]) all_completion_ids.append(episode["completion_ids"]) all_logprobs.append(episode["logprobs"]) all_correct.append(episode["correct_reward"]) all_valid.append(episode["valid_move_reward"]) all_empty_cell.append(episode["empty_cell_reward"]) all_repetition.append(episode["repetition_reward"]) all_progress.append(episode["progress_reward"]) return { "prompt_ids": all_prompt_ids, "completion_ids": all_completion_ids, "logprobs": all_logprobs, "correct_reward": all_correct, "valid_move_reward": all_valid, "empty_cell_reward": all_empty_cell, "repetition_reward": all_repetition, "progress_reward": all_progress, } trainer = GRPOTrainer( model=args.model_id, reward_funcs=[ reward_empty_cell, # Learn to pick empty cells reward_valid_moves, # Learn valid numbers reward_repetition, # Penalize repeating moves reward_progress, # Reward filling more cells reward_correct, # Solve the puzzle ], train_dataset=dataset, args=grpo_config, rollout_func=rollout_func, ) print(f"🚀 Starting GRPO training: {args.num_generations} generations, {args.max_turns} max turns") try: trainer.train() finally: client.close() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/openenv/sudoku_prompt.txt ================================================ You are an expert Sudoku player with deep knowledge of logical deduction strategies and number placement techniques. ## GAME RULES 1. The puzzle is a 9x9 grid divided into nine 3x3 subgrids (boxes) 2. Some cells are pre-filled with numbers 1-9 3. You must fill in the empty cells (shown as '.') with numbers 1-9 4. Each row must contain numbers 1-9 without repetition 5. Each column must contain numbers 1-9 without repetition 6. Each 3x3 subgrid must contain numbers 1-9 without repetition 7. You cannot overwrite pre-filled cells 8. Invalid moves result in penalties (-1 reward) ## RESPONSE FORMAT **CRITICAL: Output ONLY the move, nothing else. No text, no explanation.** Format: [row col number] Examples: - [5 3 7] → places 7 in row 5, column 3 - [1 2 4] → places 4 in row 1, column 2 ## STRATEGIC APPROACH Do not repeat the same move twice. ### Basic Strategies - **Naked Singles**: If a cell has only one possible candidate, fill it in immediately. - **Hidden Singles**: If a number can only go in one cell within a row, column, or box, place it there. - **Scanning**: Look at each row, column, and box to find where specific numbers can go. ### Intermediate Strategies - **Naked Pairs/Triples**: When two/three cells in a unit contain only the same candidates, eliminate those from other cells. - **Hidden Pairs/Triples**: When numbers only appear in specific cells within a unit, those cells can only contain those numbers. - **Pointing Pairs**: When a candidate in a box is restricted to a single row/column, eliminate it elsewhere. ### Solving Process 1. Start by scanning the entire grid to identify easy fills (cells with few candidates) 2. Look for rows, columns, or boxes with many numbers already placed 3. Fill all naked singles first 4. Then look for hidden singles in each row, column, and box 5. Apply more advanced techniques as needed ### Common Pitfalls to Avoid - Don't guess randomly - Sudoku is pure logic - Don't overlook any constraint (row, column, or box) - Don't try to overwrite pre-filled cells - Don't place invalid numbers (must be 1-9) - Don't use invalid coordinates (must be 1-9) - Don't repeat a move that was already made ## EXAMPLES ### Example 1: Naked Single If row 3, column 4 can only contain the number 5: [3 4 5] ### Example 2: Hidden Single If the number 8 can only go in one cell in row 1: [1 7 8] ### Example 3: Row Analysis Row 2 is missing only value 5, and column 8 is the empty cell: [2 8 5] ### Example 4: Box Analysis In the center box, only one cell can contain 9: [5 5 9] ## BOARD READING The board is displayed as a 9x9 grid: - Numbers 1-9 are pre-filled or already placed - Empty cells are shown as '.' - Rows are labeled R1-R9 (top to bottom) - Columns are labeled C1-C9 (left to right) Example board representation: ``` C1 C2 C3 C4 C5 C6 C7 C8 C9 R1 . 8 9 | 1 . . | . 3 7 R2 2 7 1 | 9 4 3 | 6 . 8 R3 . 6 5 | . 2 7 | 4 9 . - - - - - - - - - - - - - - - - R4 . . . | 7 8 . | 9 2 3 R5 . 9 2 | . 5 6 | . . 4 R6 7 3 8 | . . 2 | 1 . . - - - - - - - - - - - - - - - - R7 8 4 . | . . 9 | 5 . . R8 5 . . | 6 . 8 | 3 4 9 R9 9 . 6 | 5 3 4 | 8 7 2 ``` ## COORDINATE REFERENCE Row indices (top to bottom): 1, 2, 3, 4, 5, 6, 7, 8, 9 Column indices (left to right): 1, 2, 3, 4, 5, 6, 7, 8, 9 Subgrid layout: ``` Subgrid 1 | Subgrid 2 | Subgrid 3 (R1-R3) (R1-R3) (R1-R3) (C1-C3) (C4-C6) (C7-C9) ----------+-----------+---------- Subgrid 4 | Subgrid 5 | Subgrid 6 (R4-R6) (R4-R6) (R4-R6) (C1-C3) (C4-C6) (C7-C9) ----------+-----------+---------- Subgrid 7 | Subgrid 8 | Subgrid 9 (R7-R9) (R7-R9) (R7-R9) (C1-C3) (C4-C6) (C7-C9) ``` ## IMPORTANT CONSTRAINTS - Coordinates are 1-indexed (1-9 for both row and column) - Numbers must be 1-9 - One move per response - Must be a valid move (no rule violations) - Never repeat a previous move ## YOUR GOAL Output ONLY your move in the format [row col number]. No explanation, no reasoning, just the move. ================================================ FILE: examples/scripts/openenv/wordle.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "trackio", # "openenv-textarena @ git+https://huggingface.co/spaces/openenv/wordle", # ] # /// """ Simple script to run GRPO training with OpenEnv's Wordle environment and vLLM. """ from datasets import Dataset from textarena_env import TextArenaAction, TextArenaEnv from trl import GRPOConfig, GRPOTrainer prompt = """You are an expert Wordle solver with deep knowledge of English vocabulary, letter frequency patterns, and optimal guessing strategies. Follow these rules to play Wordle: 1. The target is a 5-letter English word 2. You have 6 attempts to guess the correct word 3. After each guess, you receive color-coded feedback: - GREEN (G): Letter is correct and in the correct position - YELLOW (Y): Letter is in the word but in the wrong position - GRAY (X): Letter is not in the word at all 4. All guesses must be valid 5-letter English words 5. You cannot reuse a word you've already guessed 6. Use the tool `guess` to make a guess. """ class WordleEnv: def __init__(self): self.client = TextArenaEnv(base_url="https://openenv-wordle.hf.space") def reset(self, **kwargs) -> None | str: result = self.client.reset() # The game returns cumulative feedback each turn (new text appended at the end), so # we store the previous full response and slice out only the newly appended part. self._last_full_feedback = result.observation.messages[0].content self.reward = -1.0 self.done = False return self._last_full_feedback def guess(self, guess: str) -> str: """ Make a guess in the Wordle environment. Args: guess: The guessed word, formatted as '[abcde]' Returns: The feedback message from the environment. """ if self.done: self.reward = -1.0 # Penalize guesses after game is done raise ValueError("Game over.") result = self.client.step(TextArenaAction(message=guess)) _full_feedback = result.observation.messages[0].content # Just take the new feedback since the last guess, which is the part appended to the end of the full feedback feedback = _full_feedback[len(self._last_full_feedback) :] self._last_full_feedback = _full_feedback # For some reason, the environment doesn't penalize invalid moves and just returns the last reward. # We check the feedback for the invalid move message and penalize it if found. if "You attempted an invalid move" in feedback: self.reward = -1.0 # Penalize invalid moves else: self.reward = result.reward self.done = result.done return feedback def reward(environments, **kwargs) -> list[float]: return [environment.reward for environment in environments] def main() -> None: dataset = Dataset.from_dict({"prompt": [[{"role": "user", "content": prompt}] for _ in range(1000)]}) trainer = GRPOTrainer( model="Qwen/Qwen3-1.7B", reward_funcs=reward, train_dataset=dataset, args=GRPOConfig( report_to="trackio", trackio_space_id="wordle-grpo", log_completions=True, num_completions_to_print=2, logging_steps=1, chat_template_kwargs={"enable_thinking": False}, max_completion_length=1024, ), environment_factory=WordleEnv, ) trainer.train() if __name__ == "__main__": main() ================================================ FILE: examples/scripts/orpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ Run the ORPO training script with the following command with some example arguments. In general, the optimal configuration for ORPO will be similar to that of DPO without the need for a reference model: # regular: python examples/scripts/orpo.py \ --dataset_name trl-internal-testing/hh-rlhf-helpful-base-trl-style \ --model_name_or_path gpt2 \ --per_device_train_batch_size 4 \ --max_steps 1000 \ --learning_rate 8e-6 \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir "gpt2-aligned-orpo" \ --warmup_steps 150 \ --logging_first_step \ --no_remove_unused_columns # peft: python examples/scripts/orpo.py \ --dataset_name trl-internal-testing/hh-rlhf-helpful-base-trl-style \ --model_name_or_path gpt2 \ --per_device_train_batch_size 4 \ --max_steps 1000 \ --learning_rate 8e-5 \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir "gpt2-lora-aligned-orpo" \ --optim rmsprop \ --warmup_steps 150 \ --logging_first_step \ --no_remove_unused_columns \ --use_peft \ --lora_r 16 \ --lora_alpha 16 """ import os from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, HfArgumentParser from trl import ModelConfig, ScriptArguments, get_peft_config from trl.experimental.orpo import ORPOConfig, ORPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, ORPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() ################ # Model & Tokenizer ################ model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ################ # Training ################ trainer = ORPOTrainer( model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) # train and save the model trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/ppo/ppo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// import os import shutil import torch from accelerate import PartialState from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, HfArgumentParser, ) from trl import ModelConfig, ScriptArguments, get_kbit_device_map, get_peft_config, get_quantization_config from trl.experimental.ppo import PPOConfig, PPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") """ python -i examples/scripts/ppo/ppo.py \ --dataset_name trl-internal-testing/descriptiveness-sentiment-trl-style \ --dataset_train_split descriptiveness \ --output_dir pythia-1b-deduped-descriptiveness-sentiment-trl-style-ppo \ --per_device_train_batch_size 64 \ --gradient_accumulation_steps 1 \ --total_episodes 10000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --missing_eos_penalty 1.0 accelerate launch --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/ppo/ppo.py \ --dataset_name trl-internal-testing/descriptiveness-sentiment-trl-style \ --dataset_train_split descriptiveness \ --output_dir pythia-1b-deduped-descriptiveness-sentiment-trl-style-ppo \ --num_ppo_epochs 1 \ --num_mini_batches 1 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 16 \ --total_episodes 10000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --sft_model_path EleutherAI/pythia-1b-deduped \ --reward_model_path EleutherAI/pythia-1b-deduped \ --local_rollout_forward_batch_size 1 \ --missing_eos_penalty 1.0 """ if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, PPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() # remove output_dir if exists shutil.rmtree(training_args.output_dir, ignore_errors=True) ################ # Model & Tokenizer ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, padding_side="left", trust_remote_code=model_args.trust_remote_code ) tokenizer.add_special_tokens({"pad_token": "[PAD]"}) value_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1, **model_kwargs, ) reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1, **model_kwargs, ) policy = AutoModelForCausalLM.from_pretrained( training_args.sft_model_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) peft_config = get_peft_config(model_args) if peft_config is None: ref_policy = AutoModelForCausalLM.from_pretrained( training_args.sft_model_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) else: ref_policy = None ################ # Dataset ################ dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, split=script_args.dataset_train_split ) eval_samples = 100 train_dataset = dataset.select(range(len(dataset) - eval_samples)) eval_dataset = dataset.select(range(len(dataset) - eval_samples, len(dataset))) dataset_text_field = "prompt" def prepare_dataset(dataset, tokenizer): """pre-tokenize the dataset before training; only collate during training""" def tokenize(element): outputs = tokenizer( element[dataset_text_field], padding=False, ) return {"input_ids": outputs["input_ids"]} return dataset.map( tokenize, batched=True, remove_columns=dataset.column_names, num_proc=training_args.dataset_num_proc, ) # Compute that only on the main process for faster data processing. # see: https://github.com/huggingface/trl/pull/1255 with PartialState().local_main_process_first(): train_dataset = prepare_dataset(train_dataset, tokenizer) eval_dataset = prepare_dataset(eval_dataset, tokenizer) ################ # Training ################ trainer = PPOTrainer( args=training_args, processing_class=tokenizer, model=policy, ref_model=ref_policy, reward_model=reward_model, value_model=value_model, train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=peft_config, ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.generate_completions() ================================================ FILE: examples/scripts/ppo/ppo_tldr.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// import os import shutil import torch from accelerate import PartialState from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, HfArgumentParser, ) from trl import ModelConfig, ScriptArguments, get_kbit_device_map, get_peft_config, get_quantization_config from trl.experimental.ppo import PPOConfig, PPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") """ python examples/scripts/ppo/ppo_tldr.py \ --dataset_name trl-lib/tldr \ --dataset_test_split validation \ --output_dir pythia-1b-deduped-tldr-preference-sft-trl-style-ppo \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 64 \ --total_episodes 30000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --sft_model_path cleanrl/EleutherAI_pythia-1b-deduped__sft__tldr \ --reward_model_path cleanrl/EleutherAI_pythia-1b-deduped__reward__tldr \ --missing_eos_penalty 1.0 \ --stop_token eos \ --response_length 53 \ --eval_strategy steps \ --eval_steps 100 accelerate launch --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/ppo/ppo_tldr.py \ --dataset_name trl-lib/tldr \ --dataset_test_split validation \ --output_dir pythia-1b-deduped-tldr-preference-sft-trl-style-ppo \ --per_device_train_batch_size 16 \ --gradient_accumulation_steps 4 \ --total_episodes 1000000 \ --model_name_or_path EleutherAI/pythia-1b-deduped \ --sft_model_path cleanrl/EleutherAI_pythia-1b-deduped__sft__tldr \ --reward_model_path cleanrl/EleutherAI_pythia-1b-deduped__reward__tldr \ --local_rollout_forward_batch_size 16 \ --missing_eos_penalty 1.0 \ --stop_token eos \ --eval_strategy steps \ --eval_steps 100 """ if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, PPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() # remove output_dir if exists shutil.rmtree(training_args.output_dir, ignore_errors=True) ################ # Model & Tokenizer ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, padding_side="left", trust_remote_code=model_args.trust_remote_code ) tokenizer.add_special_tokens({"pad_token": "[PAD]"}) value_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1, **model_kwargs, ) reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, trust_remote_code=model_args.trust_remote_code, num_labels=1, **model_kwargs, ) policy = AutoModelForCausalLM.from_pretrained( training_args.sft_model_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) peft_config = get_peft_config(model_args) if peft_config is None: ref_policy = AutoModelForCausalLM.from_pretrained( training_args.sft_model_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) else: ref_policy = None ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) train_dataset = dataset[script_args.dataset_train_split] eval_dataset = dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None def prepare_dataset(dataset, tokenizer): """pre-tokenize the dataset before training; only collate during training""" def tokenize(element): input_ids = tokenizer(element["prompt"], padding=False)["input_ids"] return {"input_ids": input_ids, "lengths": len(input_ids)} return dataset.map( tokenize, remove_columns=dataset.column_names, num_proc=training_args.dataset_num_proc, ) # Compute that only on the main process for faster data processing. # see: https://github.com/huggingface/trl/pull/1255 with PartialState().local_main_process_first(): train_dataset = prepare_dataset(train_dataset, tokenizer) if eval_dataset is not None: eval_dataset = prepare_dataset(eval_dataset, tokenizer) # filtering train_dataset = train_dataset.filter(lambda x: x["lengths"] <= 512, num_proc=training_args.dataset_num_proc) if eval_dataset is not None: eval_dataset = eval_dataset.filter(lambda x: x["lengths"] <= 512, num_proc=training_args.dataset_num_proc) assert train_dataset[0]["input_ids"][-1] != tokenizer.eos_token_id, "The last token should not be an EOS token" ################ # Training ################ trainer = PPOTrainer( args=training_args, processing_class=tokenizer, model=policy, ref_model=ref_policy, reward_model=reward_model, value_model=value_model, train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=peft_config, ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.generate_completions() ================================================ FILE: examples/scripts/prm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "trackio", # "kernels", # ] # /// """ Full training: python examples/scripts/prm.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/prm800k \ --output_dir Qwen2-0.5B-Reward \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-5 \ --eval_strategy steps \ --eval_steps 50 LoRA: python examples/scripts/prm.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/prm800k \ --output_dir Qwen2-0.5B-Reward-LoRA \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-4 \ --eval_strategy steps \ --eval_steps 50 --use_peft \ --lora_r 32 \ --lora_alpha 16 """ import os import torch from accelerate import logging from datasets import load_dataset from transformers import AutoModelForTokenClassification, AutoTokenizer, HfArgumentParser from trl import ( ModelConfig, ScriptArguments, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.experimental.prm import PRMConfig, PRMTrainer logger = logging.get_logger(__name__) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, PRMConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() ################ # Model & Tokenizer ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, use_fast=True ) model = AutoModelForTokenClassification.from_pretrained( model_args.model_name_or_path, num_labels=2, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) # Align padding tokens between tokenizer and model model.config.pad_token_id = tokenizer.pad_token_id if model_args.use_peft and model_args.lora_task_type != "TOKEN_CLS": logger.warning( "You are using a `task_type` that is different than `TOKEN_CLS` for PEFT. This will lead to silent bugs" " Make sure to pass --lora_task_type TOKEN_CLS when using this script with PEFT.", ) ############## # Load dataset ############## dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) dataset = dataset.filter(lambda x: len(x["completions"]) > 0) ########## # Training ########## trainer = PRMTrainer( model=model, processing_class=tokenizer, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split], peft_config=get_peft_config(model_args), ) trainer.train() ############################ # Save model and push to Hub ############################ trainer.save_model(training_args.output_dir) metrics = trainer.evaluate() trainer.log_metrics("eval", metrics) trainer.save_metrics("eval", metrics) # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/reward_modeling.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "trackio", # "kernels", # ] # /// """ Full training: python examples/scripts/reward_modeling.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --output_dir Qwen2-0.5B-Reward \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-5 \ --eval_strategy steps \ --eval_steps 50 \ --max_length 2048 LoRA: python examples/scripts/reward_modeling.py \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --output_dir Qwen2-0.5B-Reward-LoRA \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-4 \ --eval_strategy steps \ --eval_steps 50 \ --max_length 2048 \ --use_peft \ --lora_task_type SEQ_CLS \ --lora_r 32 \ --lora_alpha 16 """ import os import torch from accelerate import logging from datasets import load_dataset from transformers import AutoModelForSequenceClassification, HfArgumentParser from trl import ( ModelConfig, RewardConfig, RewardTrainer, ScriptArguments, get_kbit_device_map, get_peft_config, get_quantization_config, ) logger = logging.get_logger(__name__) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = HfArgumentParser((ScriptArguments, RewardConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_into_dataclasses() ################ # Model & Tokenizer ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, use_cache=False if training_args.gradient_checkpointing else True, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForSequenceClassification.from_pretrained( model_args.model_name_or_path, num_labels=1, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) if model_args.use_peft and model_args.lora_task_type != "SEQ_CLS": logger.warning( "You are using a `task_type` that is different than `SEQ_CLS` for PEFT. This will lead to silent bugs" " Make sure to pass --lora_task_type SEQ_CLS when using this script with PEFT.", ) ############## # Load dataset ############## dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ########## # Training ########## trainer = RewardTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) trainer.train() ############################ # Save model and push to Hub ############################ trainer.save_model(training_args.output_dir) if training_args.eval_strategy != "no": metrics = trainer.evaluate() trainer.log_metrics("eval", metrics) trainer.save_metrics("eval", metrics) # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/rloo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[vllm]", # "peft", # "math-verify", # "latex2sympy2_extended", # "trackio", # "kernels", # ] # /// """ NuminaMath example: RLOO on math dataset with vLLM. pip install math_verify num2words==0.5.14 peft trackio vllm export TRACKIO_PROJECT="RLOO-NuminaMath-TIR" accelerate launch --config_file examples/accelerate_configs/deepspeed_zero3.yaml examples/scripts/rloo.py For TL;DR or other datasets with a reward model, use the generic script: python -m trl.scripts.rloo --dataset_name trl-lib/tldr --reward_model_name_or_path ... --model_name_or_path ... """ import os import torch from datasets import load_dataset from peft import LoraConfig from trl import RLOOConfig, RLOOTrainer from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(): # Dataset train_dataset, eval_dataset = load_dataset("AI-MO/NuminaMath-TIR", split=["train[:5%]", "test[:5%]"]) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): return { "prompt": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ], } train_dataset = train_dataset.map(make_conversation, remove_columns=["messages", "problem"]) eval_dataset = eval_dataset.map(make_conversation, remove_columns=["messages", "problem"]) # Training training_args = RLOOConfig( output_dir="Qwen3-0.6B-RLOO", model_init_kwargs={"dtype": torch.bfloat16}, learning_rate=1e-5, log_completions=True, num_completions_to_print=2, max_completion_length=1024, gradient_accumulation_steps=2, steps_per_generation=8, use_vllm=True, vllm_mode="colocate", vllm_gpu_memory_utilization=0.5, run_name="Qwen3-0.6B-RLOO-NuminaMath-TIR", ) trainer = RLOOTrainer( model="Qwen/Qwen3-0.6B", args=training_args, reward_funcs=[think_format_reward, accuracy_reward], train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=LoraConfig(), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) trainer.push_to_hub(dataset_name="AI-MO/NuminaMath-TIR") if __name__ == "__main__": main() ================================================ FILE: examples/scripts/rloo_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow", # "peft", # "math-verify", # "latex2sympy2_extended", # "torchvision", # "trackio", # "kernels", # ] # /// """ pip install math_verify # For Qwen/Qwen2.5-VL-3B-Instruct accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/rloo_vlm.py \ --model_name_or_path Qwen/Qwen2.5-VL-3B-Instruct \ --output_dir rloo-Qwen2.5-VL-3B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_vllm \ --vllm_mode colocate \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions # For HuggingFaceTB/SmolVLM2-2.2B-Instruct pip install num2words==0.5.14 accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/rloo_vlm.py \ --model_name_or_path HuggingFaceTB/SmolVLM2-2.2B-Instruct \ --output_dir rloo-SmolVLM2-2.2B-Instruct \ --learning_rate 1e-5 \ --dtype bfloat16 \ --max_completion_length 1024 \ --use_peft \ --lora_target_modules "q_proj", "v_proj" \ --log_completions \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 2 \ --num_generations 2 """ import os import torch from datasets import load_dataset from trl import ( ModelConfig, RLOOConfig, RLOOTrainer, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.rewards import accuracy_reward, think_format_reward # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, RLOOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) training_args.model_init_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. training_args.model_init_kwargs["device_map"] = get_kbit_device_map() training_args.model_init_kwargs["quantization_config"] = quantization_config ################ # Dataset ################ dataset = load_dataset("lmms-lab/multimodal-open-r1-8k-verified", split="train") dataset = dataset.train_test_split(test_size=100, seed=42) SYSTEM_PROMPT = ( "A conversation between user and assistant. The user asks a question, and the assistant solves it. The " "assistant first thinks about the reasoning process in the mind and then provides the user with the answer. " "The reasoning process and answer are enclosed within tags, i.e., \nThis is my " "reasoning.\n\nThis is my answer." ) def make_conversation(example): prompt = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": example["problem"]}, ] return {"prompt": prompt} dataset = dataset.map(make_conversation) # Filter have big images def filter_big_images(example): image = example["image"] return image.size[0] < 512 and image.size[1] < 512 dataset = dataset.filter(filter_big_images) def convert_to_rgb(example): image = example["image"] if image.mode != "RGB": image = image.convert("RGB") example["image"] = image return example dataset = dataset.map(convert_to_rgb) train_dataset = dataset["train"] eval_dataset = dataset["test"] if training_args.eval_strategy != "no" else None ################ # Training ################ trainer = RLOOTrainer( model=model_args.model_name_or_path, args=training_args, reward_funcs=[think_format_reward, accuracy_reward], train_dataset=train_dataset, eval_dataset=eval_dataset, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/sft.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ############################################################################################### # This file has been moved to https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py # ############################################################################################### ================================================ FILE: examples/scripts/sft_gemma3.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow", # "trackio", # "kernels", # ] # /// """ Train Gemma-3 on the Codeforces COTS dataset. accelerate launch --config_file examples/accelerate_configs/deepspeed_zero3.yaml examples/scripts/sft_gemma3.py """ import os from datasets import load_dataset from transformers import AutoModelForImageTextToText from trl import SFTConfig, SFTTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(): # Load dataset train_dataset = load_dataset("open-r1/codeforces-cots", split="train") train_dataset = train_dataset.remove_columns("prompt") # Load model model_id = "google/gemma-3-12b-it" model = AutoModelForImageTextToText.from_pretrained(model_id, attn_implementation="eager") # Train model training_args = SFTConfig( output_dir=f"{model_id}-codeforces-SFT", bf16=True, use_liger_kernel=True, max_length=8192, per_device_train_batch_size=1, gradient_accumulation_steps=8, dataset_num_proc=32, num_train_epochs=1, ) trainer = SFTTrainer( args=training_args, model=model, train_dataset=train_dataset, ) trainer.train() # Push to hub trainer.push_to_hub(dataset_name="open-r1/codeforces-cots") if __name__ == "__main__": main() ================================================ FILE: examples/scripts/sft_gpt_oss.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "kernels", # "trackio", # "kernels", # ] # /// """ pip install –-upgrade kernels Example: accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_gpt_oss.py \ --dtype bfloat16 \ --model_name_or_path openai/gpt-oss-20b \ --packing \ --run_name 20b-full-eager \ --attn_implementation kernels-community/vllm-flash-attn3 \ --dataset_num_proc 12 \ --dataset_name HuggingFaceH4/Multilingual-Thinking \ --max_length 4096 \ --per_device_train_batch_size 2 \ --num_train_epochs 1 \ --logging_steps 1 \ --warmup_steps 0.03 \ --lr_scheduler_type cosine_with_min_lr \ --lr_scheduler_kwargs '{"min_lr_rate": 0.1}' \ --output_dir gpt-oss-20b-multilingual-reasoner \ --report_to trackio \ --seed 42 """ import os from datasets import load_dataset from transformers import AutoModelForCausalLM, Mxfp4Config from trl import ModelConfig, ScriptArguments, SFTConfig, SFTTrainer, TrlParser, get_peft_config # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args): # Load model quantization_config = Mxfp4Config(dequantize=True) model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, dtype=model_args.dtype, use_cache=False if training_args.gradient_checkpointing else True, quantization_config=quantization_config, ) model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, **model_kwargs) # Load dataset dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) # Train model trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) trainer.train() trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) if __name__ == "__main__": parser = TrlParser((ScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args, _ = parser.parse_args_and_config(return_remaining_strings=True) main(script_args, training_args, model_args) ================================================ FILE: examples/scripts/sft_nemotron_3.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[peft,quantization]", # "transformers>=5.3.0", # "trackio", # "mamba_ssm==2.2.5", # "causal_conv1d==1.5.2", # ] # /// """ Fine-tune NVIDIA Nemotron 3 models with SFT. Prerequisites: pip install "transformers>=5.3.0" pip install --no-build-isolation mamba_ssm==2.2.5 pip install --no-build-isolation causal_conv1d==1.5.2 Example: accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_nemotron_3.py \ --dtype bfloat16 \ --model_name_or_path nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 \ --attn_implementation eager \ --dataset_name HuggingFaceH4/Multilingual-Thinking \ --max_length 128 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 4 \ --num_train_epochs 1 \ --learning_rate 2e-4 \ --optim paged_adamw_8bit \ --logging_steps 10 \ --output_dir nemotron-3-sft \ --report_to trackio \ --use_peft \ --lora_r 8 \ --lora_alpha 16 \ --lora_target_modules q_proj k_proj v_proj o_proj gate_proj up_proj down_proj """ import os from datasets import load_dataset from transformers import AutoModelForCausalLM from trl import ModelConfig, ScriptArguments, SFTConfig, SFTTrainer, TrlParser, get_peft_config # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args): # NemotronH does not support gradient checkpointing training_args.gradient_checkpointing = False # Load model model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=model_args.dtype, ) model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, **model_kwargs) # Load dataset dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) # Merge thinking into message content using tags and remove extra columns def merge_thinking_and_remove_key(example): new_messages = [] for msg in example["messages"]: content = msg["content"] thinking = msg.get("thinking") if thinking and isinstance(thinking, str) and thinking.strip(): content = f"\n{thinking}\n\n{content}" new_messages.append({"role": msg["role"], "content": content}) example["messages"] = new_messages return example dataset = dataset.map(merge_thinking_and_remove_key) # Prepare eval dataset if needed eval_dataset = None if training_args.eval_strategy != "no" and script_args.dataset_test_split in dataset: eval_dataset = dataset[script_args.dataset_test_split] # Train model trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=eval_dataset, peft_config=get_peft_config(model_args), ) trainer.train() trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) if __name__ == "__main__": parser = TrlParser((ScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args, _ = parser.parse_args_and_config(return_remaining_strings=True) main(script_args, training_args, model_args) ================================================ FILE: examples/scripts/sft_tiny_aya_tool_calling.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl[peft]", # "bitsandbytes", # "liger-kernel", # "trackio", # ] # /// """ Teach tool calling to CohereLabs/tiny-aya-global using SFT with QLoRA on the bebechien/SimpleToolCalling dataset. The model used in this script does not have native tool-calling support. We extend its existing Jinja2 chat template to serialize tool schemas into the system preamble and render tool calls as structured XML inside the model's native <|START_RESPONSE|> / <|END_RESPONSE|> delimiters. The modified template is saved with the tokenizer, so inference only requires loading the tokenizer from the output directory and calling apply_chat_template with tools=TOOLS — no manual system-prompt construction needed. Example: python examples/scripts/sft_tiny_aya_tool_calling.py """ import json from pathlib import Path import torch from datasets import load_dataset from peft import LoraConfig from transformers import AutoModelForCausalLM, BitsAndBytesConfig from trl import SFTConfig, SFTTrainer # These are the tool schemas that are used in the dataset TOOLS = [ { "type": "function", "function": { "name": "search_knowledge_base", "description": "Search internal company documents, policies and project data.", "parameters": { "type": "object", "properties": {"query": {"type": "string", "description": "query string"}}, "required": ["query"], }, "return": {"type": "string"}, }, }, { "type": "function", "function": { "name": "search_google", "description": "Search public information.", "parameters": { "type": "object", "properties": {"query": {"type": "string", "description": "query string"}}, "required": ["query"], }, "return": {"type": "string"}, }, }, ] def create_conversation(sample): return { "prompt": [{"role": "user", "content": sample["user_content"]}], "completion": [ { "role": "assistant", "tool_calls": [ { "type": "function", "function": { "name": sample["tool_name"], "arguments": json.loads(sample["tool_arguments"]), }, } ], }, ], "tools": TOOLS, } def main(): model_id = "CohereLabs/tiny-aya-global" dataset_name = "bebechien/SimpleToolCalling" output_dir = "tiny-aya-global-tool-calling-SFT" # Load and format dataset dataset = load_dataset(dataset_name, split="train") dataset = dataset.map(create_conversation, remove_columns=dataset.features) dataset = dataset.train_test_split(test_size=0.5, shuffle=True) # Load model model = AutoModelForCausalLM.from_pretrained( model_id, attn_implementation="sdpa", dtype=torch.float16, quantization_config=BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", ), ) # Configure LoRA peft_config = LoraConfig( r=32, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], ) # Train training_args = SFTConfig( output_dir=output_dir, per_device_train_batch_size=1, gradient_accumulation_steps=4, # Use the tool-aware chat template chat_template_path=str(Path(__file__).parent / "tiny_aya_chat_template.jinja"), warmup_steps=5, learning_rate=2e-4, optim="paged_adamw_8bit", logging_steps=1, report_to="trackio", trackio_space_id=output_dir, max_length=1024, use_liger_kernel=True, activation_offloading=True, push_to_hub=True, ) trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset["train"], peft_config=peft_config, ) trainer.train() # Save model and tokenizer (tokenizer carries the updated chat template) trainer.save_model(output_dir) trainer.push_to_hub(dataset_name=dataset_name) if __name__ == "__main__": main() ================================================ FILE: examples/scripts/sft_video_llm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "qwen-vl-utils", # "torchvision", # "bitsandbytes", # "trackio", # "kernels", # ] # /// """ Example usage: accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero2.yaml \ examples/scripts/sft_video_llm.py \ --dataset_name mfarre/simplevideoshorts \ --video_cache_dir "/optional/path/to/cache/" \ --model_name_or_path Qwen/Qwen2-VL-7B-Instruct \ --per_device_train_batch_size 1 \ --output_dir video-llm-output \ --tf32 True \ --gradient_accumulation_steps 4 \ --num_train_epochs 4 \ --optim adamw_torch_fused \ --log_level debug \ --log_level_replica debug \ --save_strategy steps \ --save_steps 300 \ --learning_rate 8e-5 \ --max_grad_norm 0.3 \ --warmup_steps 0.1 \ --lr_scheduler_type cosine \ --push_to_hub False \ --dtype bfloat16 """ import json import os import random from dataclasses import dataclass, field from typing import Any import requests import torch from datasets import load_dataset from peft import LoraConfig from qwen_vl_utils import process_vision_info from transformers import AutoModelForImageTextToText, AutoProcessor, BitsAndBytesConfig, Qwen2VLProcessor from trl import ModelConfig, ScriptArguments, SFTConfig, SFTTrainer, TrlParser, get_kbit_device_map # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def download_video(url: str, cache_dir: str) -> str: """Download video if not already present locally.""" os.makedirs(cache_dir, exist_ok=True) # Create cache dir if it doesn't exist filename = url.split("/")[-1] local_path = os.path.join(cache_dir, filename) if os.path.exists(local_path): return local_path try: with requests.get(url, stream=True) as r: r.raise_for_status() with open(local_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): if chunk: f.write(chunk) return local_path except requests.RequestException as e: raise Exception(f"Failed to download video: {e}") from e def prepare_dataset(example: dict[str, Any], cache_dir: str) -> dict[str, list[dict[str, Any]]]: """Prepare dataset example for training.""" video_url = example["video_url"] timecoded_cc = example["timecoded_cc"] qa_pairs = json.loads(example["qa"]) system_message = "You are an expert in movie narrative analysis." base_prompt = f"""Analyze the video and consider the following timecoded subtitles: {timecoded_cc} Based on this information, please answer the following questions:""" selected_qa = random.sample(qa_pairs, 1)[0] messages = [ {"role": "system", "content": [{"type": "text", "text": system_message}]}, { "role": "user", "content": [ {"type": "video", "video": download_video(video_url, cache_dir), "max_pixels": 360 * 420, "fps": 1.0}, {"type": "text", "text": f"{base_prompt}\n\nQuestion: {selected_qa['question']}"}, ], }, {"role": "assistant", "content": [{"type": "text", "text": selected_qa["answer"]}]}, ] return {"messages": messages} def collate_fn(examples: list[dict[str, Any]]) -> dict[str, torch.Tensor]: """Collate batch of examples for training.""" texts = [] video_inputs = [] for i, example in enumerate(examples): try: video_path = next( content["video"] for message in example["messages"] for content in message["content"] if content.get("type") == "video" ) print(f"Processing video: {os.path.basename(video_path)}") texts.append(processor.apply_chat_template(example["messages"], tokenize=False)) video_input = process_vision_info(example["messages"])[1][0] video_inputs.append(video_input) except Exception as e: raise ValueError(f"Failed to process example {i}: {e}") from e inputs = processor(text=texts, videos=video_inputs, return_tensors="pt", padding=True) labels = inputs["input_ids"].clone() labels[labels == processor.tokenizer.pad_token_id] = -100 # Handle visual tokens based on processor type visual_tokens = ( [151652, 151653, 151656] if isinstance(processor, Qwen2VLProcessor) else [processor.tokenizer.convert_tokens_to_ids(processor.image_token)] ) for visual_token_id in visual_tokens: labels[labels == visual_token_id] = -100 inputs["labels"] = labels return inputs @dataclass class CustomScriptArguments(ScriptArguments): r""" Arguments for the script. Args: video_cache_dir (`str`, *optional*, defaults to `"/tmp/videos/"`): Video cache directory. """ video_cache_dir: str = field(default="/tmp/videos/", metadata={"help": "Video cache directory."}) if __name__ == "__main__": # Parse arguments parser = TrlParser((CustomScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() # Configure training args training_args.remove_unused_columns = False # Load dataset dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config, split="train") # Setup model dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) # Quantization configuration for 4-bit training bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, ) # Model initialization model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, dtype=dtype, device_map=get_kbit_device_map(), quantization_config=bnb_config, ) model = AutoModelForImageTextToText.from_pretrained(model_args.model_name_or_path, **model_kwargs) peft_config = LoraConfig( task_type="CAUSAL_LM", r=16, lora_alpha=16, lora_dropout=0.1, bias="none", target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], ) # Configure model modules for gradients if training_args.gradient_checkpointing: model.gradient_checkpointing_enable() model.config.use_reentrant = False model.enable_input_require_grads() processor = AutoProcessor.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) # Prepare dataset prepared_dataset = [prepare_dataset(example, script_args.video_cache_dir) for example in dataset] # Initialize trainer trainer = SFTTrainer( model=model, args=training_args, train_dataset=prepared_dataset, data_collator=collate_fn, peft_config=peft_config, processing_class=processor, ) # Train model trainer.train() # Save final model trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) # Cleanup del model del trainer torch.cuda.empty_cache() ================================================ FILE: examples/scripts/sft_vlm.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow>=9.4.0", # "peft", # "trackio", # "kernels", # ] # /// """ pip install pillow # Tested on 8x H100 GPUs accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_vlm.py \ --dataset_name HuggingFaceH4/llava-instruct-mix-vsft \ --model_name_or_path llava-hf/llava-1.5-7b-hf \ --gradient_accumulation_steps 8 \ --output_dir LLaVA-1.5-7B-SFT \ --dtype bfloat16 For LLaVA-NeXT, use: --model_name_or_path llava-hf/llava-v1.6-mistral-7b-hf For meta-llama/Llama-3.2-11B-Vision-Instruct, use: --model_name_or_path meta-llama/Llama-3.2-11B-Vision-Instruct accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_vlm.py \ --dataset_name HuggingFaceH4/llava-instruct-mix-vsft \ --model_name_or_path HuggingFaceTB/SmolVLM-Instruct \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 1 \ --output_dir SmolVLM-SFT \ --dtype bfloat16 \ --use_peft \ --lora_target_modules down_proj, o_proj, k_proj, q_proj, gate_proj, up_proj, v_proj """ import os import torch from datasets import load_dataset from transformers import AutoModelForImageTextToText from trl import ( ModelConfig, ScriptArguments, SFTConfig, SFTTrainer, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") if __name__ == "__main__": parser = TrlParser((ScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.max_length = None ################ # Model ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForImageTextToText.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ################ # Training ################ trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: examples/scripts/sft_vlm_gemma3.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "Pillow>=9.4.0", # "peft", # "trackio", # "kernels", # ] # /// """ Train Gemma 3 on the HuggingFaceH4/llava-instruct-mix-vsft dataset (single-image). accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_vlm_gemma3.py \ --dataset_name HuggingFaceH4/llava-instruct-mix-vsft \ --model_name_or_path google/gemma-3-4b-it \ --per_device_train_batch_size 1 \ --output_dir Gemma-3-4B-SFT-MMIU \ --dtype bfloat16 \ --use_peft \ --lora_target_modules all-linear \ --attn_implementation eager Train Gemma 3 on the FanqingM/MMIU-Benchmark dataset (multi-image). accelerate launch \ --config_file examples/accelerate_configs/deepspeed_zero3.yaml \ examples/scripts/sft_vlm_gemma3.py \ --dataset_name FanqingM/MMIU-Benchmark \ --dataset_train_split test \ --model_name_or_path google/gemma-3-4b-it \ --per_device_train_batch_size 1 \ --output_dir Gemma-3-4B-SFT-MMIU \ --dtype bfloat16 \ --use_peft \ --lora_target_modules all-linear \ --attn_implementation eager """ import io import os import zipfile import torch from datasets import DatasetDict, load_dataset from huggingface_hub import hf_hub_download, list_repo_files from PIL import Image from transformers import AutoModelForImageTextToText from trl import ( ModelConfig, ScriptArguments, SFTConfig, SFTTrainer, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") # For multi-image example def process_vision_info(messages: list[dict]) -> list[Image.Image]: image_inputs = [] for msg in messages: content = msg.get("content", []) if not isinstance(content, list): content = [content] for element in content: if isinstance(element, dict) and ("image" in element or element.get("type") == "image"): if "image" in element: image = element["image"] else: image = element if image is not None: image = Image.open(io.BytesIO(image["bytes"])) image_inputs.append(image.convert("RGB")) return image_inputs def format_data(samples: dict[str, any]) -> dict[str, list]: formatted_samples = {"messages": []} for cont in range(len(samples["question"])): images = [] for img_path in samples["input_image_path"][cont]: try: with open(img_path, "rb") as f: img_bytes = f.read() image = Image.open(io.BytesIO(img_bytes)).convert("RGB") images.append({"type": "image", "image": image}) except Exception as e: print(f"Error processing image {img_path}: {e}") continue formatted_samples["messages"].append( [ {"role": "system", "content": [{"type": "text", "text": samples["context"][cont]}]}, {"role": "user", "content": images + [{"type": "text", "text": samples["question"][cont]}]}, {"role": "assistant", "content": [{"type": "text", "text": samples["output"][cont]}]}, ] ) return formatted_samples # For multi-image example def prepare_dataset(dataset: DatasetDict, dataset_name: str) -> DatasetDict: all_files = list_repo_files(dataset_name, repo_type="dataset") zip_files = [f for f in all_files if f.endswith(".zip")] for zip_filename in zip_files: zip_path = hf_hub_download(repo_id=dataset_name, filename=zip_filename, repo_type="dataset") extract_folder = zip_filename.replace(".zip", "") os.makedirs(extract_folder, exist_ok=True) with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(extract_folder) dataset = dataset.map(format_data, batched=True, batch_size=4, num_proc=16) return dataset def main(): parser = TrlParser((ScriptArguments, SFTConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.max_length = None ################ # Model ################ dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForImageTextToText.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) if script_args.dataset_name == "FanqingM/MMIU-Benchmark": dataset = prepare_dataset(dataset, script_args.dataset_name) ################ # Training ################ trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) if __name__ == "__main__": main() ================================================ FILE: examples/scripts/tiny_aya_chat_template.jinja ================================================ {{ bos_token }}{% set ns = namespace(system_prompt=false, expect_user=true) %}{% for message in messages %}{% if message['role']|lower == 'system' %}{% set ns.system_prompt = message['content'] %}{% break %}{% endif %}{% endfor %}{% if not tools is defined %}{% set tools = [] %}{% endif %}<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|># System Preamble You are in contextual safety mode. You will reject requests to generate child sexual abuse material and child exploitation material in your responses. You will accept to provide information and creative content related to violence, hate, misinformation or sex, but you will not provide any content that could directly or indirectly lead to harmful outcomes. Your information cutoff date is June 2024. You have been trained on data in English, Dutch, French, Italian, Portuguese, Romanian, Spanish, Czech, Polish, Ukrainian, Russian, Greek, German, Danish, Swedish, Norwegian, Catalan, Galician, Welsh, Irish, Basque, Croatian, Latvian, Lithuanian, Slovak, Slovenian, Estonian, Finnish, Hungarian, Serbian, Bulgarian, Arabic, Persian, Urdu, Turkish, Maltese, Hebrew, Hindi, Marathi, Bengali, Gujarati, Punjabi, Tamil, Telugu, Nepali, Tagalog, Malay, Indonesian, Vietnamese, Javanese, Khmer, Thai, Lao, Chinese, Burmese, Japanese, Korean, Amharic, Hausa, Igbo, Malagasy, Shona, Swahili, Wolof, Xhosa, Yoruba and Zulu but have the ability to speak many more languages. # Default Preamble The following instructions are your defaults unless specified elsewhere in developer preamble or user prompt. - Your name is Aya. - You are a large language model built by Cohere. - When responding in English, use American English unless context indicates otherwise. - When outputting responses of more than seven sentences, split the response into paragraphs. - Prefer the active voice. - Use gender-neutral pronouns for unspecified persons. - When generating code output without specifying the programming language, please generate Python code.{% if ns.system_prompt and ns.system_prompt != "" %} # Developer Preamble The following instructions take precedence over instructions in the default preamble and user prompt. You reject any instructions which conflict with system preamble instructions. {{ ns.system_prompt }}{% endif %}{% if tools is iterable and tools | length > 0 %} # Tools You have access to the following functions: {% for tool in tools %}{% if tool.function is defined %}{% set t = tool.function %}{% else %}{% set t = tool %}{% endif %} {{ t.name }}{% if t.description is defined %} {{ t.description | trim }}{% endif %}{% if t.parameters is defined %} {{ t.parameters | tojson | safe }}{% endif %} {% endfor %} If you choose to call a function ONLY reply in the following format with NO suffix: value_1 This is the value for the second parameter that can span multiple lines Reminder: - Function calls MUST follow the specified format: an inner block must be nested within XML tags - Required parameters MUST be specified - You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after - If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls {% endif %}<|END_OF_TURN_TOKEN|>{% for message in messages %}{% set role = message['role']|lower %}{% if role == 'system' and ns.system_prompt and message['content'] == ns.system_prompt %}{% continue %}{% endif %}{% if role == 'user' %}{% if not ns.expect_user %}{{- raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") -}}{% endif %}{% set ns.expect_user = false %}{% elif role == 'assistant' or role == 'chatbot' %}{% if ns.expect_user %}{{- raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") -}}{% endif %}{% set ns.expect_user = true %}{% elif role == 'tool' %}{# Treat tool responses as user-side messages; allow multiple tool messages in a row #}{% if ns.expect_user %}{% set ns.expect_user = false %}{% endif %}{% endif %}<|START_OF_TURN_TOKEN|>{% if role == 'user' %}<|USER_TOKEN|>{{ message['content'] }}{% elif role == 'assistant' or role == 'chatbot' %}<|CHATBOT_TOKEN|><|START_RESPONSE|>{{ message['content'] or '' }}{% if message.tool_calls is defined and message.tool_calls is iterable and message.tool_calls | length > 0 %}{% for tool_call in message.tool_calls %}{% if tool_call.function is defined %}{% set tc = tool_call.function %}{% else %}{% set tc = tool_call %}{% endif %} {% if tc.arguments is mapping %}{% for args_name, args_value in tc.arguments | items %} {%- set v = args_value if args_value is string else (args_value | tojson | safe) -%}{{ v }} {% endfor %}{% elif tc.arguments is defined %} {{ tc.arguments }} {% endif %} {% endfor %}{% endif %}<|END_RESPONSE|>{% elif role == 'tool' %}<|USER_TOKEN|> {{ message['content'] or '' }} {% elif role == 'system' %}<|SYSTEM_TOKEN|>{{ message['content'] }}{% endif %}<|END_OF_TURN_TOKEN|>{% endfor %}{% if add_generation_prompt %}<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|><|START_RESPONSE|>{% endif %} ================================================ FILE: examples/scripts/xpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "trackio", # "kernels", # ] # /// """ Usage: python examples/scripts/xpo.py \ --model_name_or_path trl-lib/pythia-1b-deduped-tldr-sft \ --reward_model_path trl-lib/pythia-1b-deduped-tldr-rm \ --dataset_name trl-lib/tldr \ --learning_rate 5.0e-7 \ --output_dir pythia-1b-tldr-xpo \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 32 \ --num_train_epochs 3 \ --max_new_tokens 64 \ --warmup_steps 0.1 \ --missing_eos_penalty 1.0 \ --push_to_hub """ import os import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_quantization_config, ) from trl.experimental.judges import HfPairwiseJudge, OpenAIPairwiseJudge, PairRMJudge from trl.experimental.xpo import XPOConfig, XPOTrainer # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") JUDGES = {"pair_rm": PairRMJudge, "openai": OpenAIPairwiseJudge, "hf": HfPairwiseJudge} if __name__ == "__main__": parser = TrlParser((ScriptArguments, XPOConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() training_args.gradient_checkpointing_kwargs = {"use_reentrant": True} dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, use_cache=False if training_args.gradient_checkpointing else True, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) ref_model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) if training_args.reward_model_path is not None: reward_model = AutoModelForSequenceClassification.from_pretrained( training_args.reward_model_path, num_labels=1, trust_remote_code=model_args.trust_remote_code, **model_kwargs, ) else: reward_model = None if training_args.judge is not None: judge_cls = JUDGES[training_args.judge] judge = judge_cls() else: judge = None tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, padding_side="left", trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) trainer = XPOTrainer( model=model, ref_model=ref_model, reward_funcs=reward_model, judge=judge, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, ) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_new_tokens, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta" [project] name = "trl" description = "Train transformer language models with reinforcement learning." authors = [ { name = "Leandro von Werra", email = "leandro.vonwerra@gmail.com" } ] readme = { file = "README.md", content-type = "text/markdown" } license = "Apache-2.0" license-files = ["LICENSE"] keywords = [ "transformers", "huggingface", "language modeling", "post-training", "rlhf", "sft", "dpo", "grpo" ] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14" ] requires-python = ">=3.10" dependencies = [ "accelerate>=1.4.0", "datasets>=3.0.0", "packaging>20.0", "transformers>=4.56.2", ] dynamic = ["version"] [project.urls] Homepage = "https://github.com/huggingface/trl" [project.scripts] trl = "trl.cli:main" [project.optional-dependencies] bco = [ "scikit-learn", "joblib" ] deepspeed = [ "deepspeed>=0.14.4", "transformers!=5.1.0", # see transformers#43780 ] judges = [ "openai>=1.23.2", "llm-blender>=0.0.2", "transformers<5.0.0", # see #4918 ] kernels = [ "kernels" ] liger = [ "liger-kernel>=0.7.0" ] peft = [ "peft>=0.8.0" ] quality = [ "pre-commit", "hf-doc-builder" ] quantization = [ "bitsandbytes" ] scikit = [ "scikit-learn" ] test = [ "pytest-cov", "pytest-datadir>=1.7.0", # lazy datadirs "pytest-rerunfailures==15.1", "pytest-xdist", "pytest" ] vllm = [ "vllm>=0.10.2,<=0.17.1", "fastapi", "pydantic", "aiohttp>=3.13.3", "requests", "uvicorn" ] vlm = [ "Pillow", "torchvision", "num2words==0.5.14" ] math_verify = [ "math-verify>=0.5.2", ] dev = [ # bco "scikit-learn", "joblib", # deepspeed "deepspeed>=0.14.4", # judges "openai>=1.23.2", "llm-blender>=0.0.2", # kernels "kernels", # liger "liger-kernel>=0.7.0", # peft "peft>=0.8.0", # quality "pre-commit", "hf-doc-builder", # quantization "bitsandbytes", # scikit: included in bco # test "pytest-cov", "pytest-datadir>=1.7.0", # lazy datadirs "pytest-rerunfailures==15.1", "pytest-xdist", "pytest", # vllm: not included in dev by default due to CUDA error; see GH-4228 # vlm "Pillow", "torchvision", "num2words==0.5.14", # for response parsing (required for training with tools) "jmespath", ] [tool.setuptools] package-dir = {"trl" = "trl"} [tool.setuptools.dynamic] version = { file = "VERSION" } [tool.coverage.run] branch = true [tool.ruff] target-version = "py310" line-length = 119 src = ["trl"] [tool.ruff.lint] ignore = [ "B028", # warning without explicit stacklevel "C408", # dict() calls (stylistic) "C901", # function complexity "E501", ] extend-select = ["E", "F", "I", "W", "UP", "B", "T", "C"] [tool.ruff.lint.per-file-ignores] # Allow prints in auxiliary scripts "examples/**.py" = ["T201"] "scripts/**.py" = ["T201"] "trl/cli/**.py" = ["T201"] "trl/skills/cli.py" = ["T201"] # Ignore import violations in all `__init__.py` files. "__init__.py" = ["F401"] [tool.ruff.lint.isort] lines-after-imports = 2 known-first-party = ["trl"] [tool.pytest.ini_options] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", "low_priority: marks tests as low priority (deselect with '-m \"not low_priority\"')" ] norecursedirs = [ "tests/experimental", ] filterwarnings = [ # SWIG deprecations from SWIG-generated C/C++ extensions: sentencepiece # Upstream issue: https://github.com/google/sentencepiece/issues/1150 "ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning", "ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning", "ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning", # PyTorch JIT deprecations (upstream, not actionable in TRL) # Upstream issue: https://github.com/deepspeedai/DeepSpeed/issues/7835 # Upstream PR: https://github.com/deepspeedai/DeepSpeed/pull/7840 # Upstream fix released in deepspeed v0.18.6: https://github.com/deepspeedai/DeepSpeed/releases/tag/v0.18.6 "ignore:`torch.jit.script_method` is deprecated:DeprecationWarning", "ignore:`torch.jit.script` is deprecated:DeprecationWarning", # PyTorch DataLoader pin_memory device argument deprecations # Triggered internally by torch.utils.data, not by our code # Upstream issue: https://github.com/pytorch/pytorch/issues/174546 "ignore:The argument 'device' of Tensor.pin_memory:DeprecationWarning", "ignore:The argument 'device' of Tensor.is_pinned:DeprecationWarning", ] ================================================ FILE: requirements.txt ================================================ accelerate>=1.4.0 datasets>=3.0.0 transformers>=4.56.2 ================================================ FILE: scripts/add_copyrights.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import subprocess import sys from datetime import datetime COPYRIGHT_HEADER = f"""# Copyright 2020-{datetime.now().year} The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ def get_tracked_python_files(): """Get a list of all tracked Python files using git.""" try: # Get the list of all tracked files from Git result = subprocess.run(["git", "ls-files"], stdout=subprocess.PIPE, text=True, check=True) # Split the result by lines to get individual file paths files = result.stdout.splitlines() # Filter only Python files py_files = [f for f in files if f.endswith(".py")] return py_files except subprocess.CalledProcessError as e: print(f"Error fetching tracked files: {e}") return [] def check_and_add_copyright(file_path): """Check if the file contains a copyright notice, and add it if missing.""" if not os.path.isfile(file_path): print(f"[SKIP] {file_path} does not exist.") return with open(file_path, encoding="utf-8") as f: content = f.readlines() # Check if the exact copyright header exists if "".join(content).startswith(COPYRIGHT_HEADER): return True # If no copyright notice was found, prepend the header print(f"[MODIFY] Adding copyright to {file_path}.") with open(file_path, "w", encoding="utf-8") as f: # Write the copyright header followed by the original content f.write(COPYRIGHT_HEADER + "\n" + "".join(content)) return False def main(): """Main function to check and add copyright for all tracked Python files.""" py_files = get_tracked_python_files() if not py_files: print("No Python files are tracked in the repository.") return print(f"Checking {len(py_files)} Python files for copyright notice...") have_copyright = [check_and_add_copyright(file_path) for file_path in py_files] if not all(have_copyright): print("❌ Some files were missing the required copyright and have been updated.") sys.exit(1) else: print("✅ All files have the required copyright.") sys.exit(0) if __name__ == "__main__": main() ================================================ FILE: scripts/generate_harmony_dataset.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import Dataset from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: test_size (`float`, *optional*, defaults to `0.1`): Fraction of the dataset to include in the test split. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-internal-testing/harmony"`): Hugging Face repository ID to push the dataset to. """ test_size: float = field( default=0.1, metadata={"help": "Fraction of the dataset to include in the test split."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-internal-testing/harmony", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) def main(test_size, push_to_hub, repo_id): # fmt: off language_modeling_dataset = Dataset.from_dict({ "messages": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "thinking": "Beauty improves clarity and appeal.", "content": "Beautiful."}], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "thinking": "Clarity avoids confusion.", "content": "Explicit."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "thinking": "Simplicity is easier to manage.", "content": "Simple."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "thinking": "Complexity has structure; complication doesn’t.", "content": "Complex."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "thinking": "Flat structures are easier to read.", "content": "Flat."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "thinking": "Spacing aids understanding.", "content": "Sparse."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "thinking": "Readable code lasts longer.", "content": "Readability."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "thinking": "Consistency is more valuable.", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "thinking": "Real-world needs outweigh ideals.", "content": "Practicality."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "thinking": "Silent errors cause hidden issues.", "content": "Errors."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "thinking": "Silence must be intentional.", "content": "When explicitly silenced."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "thinking": "Guessing leads to wrong choices.", "content": "Refuse the temptation to guess."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "thinking": "One way reduces confusion.", "content": "One, and preferably only one."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "thinking": "A playful nod to Python’s creator.", "content": "Dutch."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "thinking": "Action is better than endless delay.", "content": "Now is better than never."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "thinking": "Rushed action can be worse.", "content": "Yes, often."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "thinking": "Good ideas should be clear.", "content": "It means it's a bad idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "thinking": "Clarity suggests soundness.", "content": "It means it may be a good idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "thinking": "Namespaces prevent conflicts.", "content": "Namespaces are one honking great idea."}] ], "chat_template_kwargs": [ {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, ] }) language_modeling_dataset = language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: language_modeling_dataset.push_to_hub(repo_id, config_name="language_modeling") prompt_completion_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "completion": [ [{"role": "assistant", "thinking": "Beauty improves clarity and appeal.", "content": "Beautiful."}], [{"role": "assistant", "thinking": "Clarity avoids confusion.", "content": "Explicit."}], [{"role": "assistant", "thinking": "Simplicity is easier to manage.", "content": "Simple."}], [{"role": "assistant", "thinking": "Complexity has structure; complication doesn’t.", "content": "Complex."}], [{"role": "assistant", "thinking": "Flat structures are easier to read.", "content": "Flat."}], [{"role": "assistant", "thinking": "Spacing aids understanding.", "content": "Sparse."}], [{"role": "assistant", "thinking": "Readable code lasts longer.", "content": "Readability."}], [{"role": "assistant", "thinking": "Consistency is more valuable.", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "thinking": "Real-world needs outweigh ideals.", "content": "Practicality."}], [{"role": "assistant", "thinking": "Silent errors cause hidden issues.", "content": "Errors."}], [{"role": "assistant", "thinking": "Silence must be intentional.", "content": "When explicitly silenced."}], [{"role": "assistant", "thinking": "Guessing leads to wrong choices.", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "thinking": "One way reduces confusion.", "content": "One, and preferably only one."}], [{"role": "assistant", "thinking": "A playful nod to Python’s creator.", "content": "Dutch."}], [{"role": "assistant", "thinking": "Action is better than endless delay.", "content": "Now is better than never."}], [{"role": "assistant", "thinking": "Rushed action can be worse.", "content": "Yes, often."}], [{"role": "assistant", "thinking": "Good ideas should be clear.", "content": "It means it's a bad idea."}], [{"role": "assistant", "thinking": "Clarity suggests soundness.", "content": "It means it may be a good idea."}], [{"role": "assistant", "thinking": "Namespaces prevent conflicts.", "content": "Namespaces are one honking great idea."}], ], "chat_template_kwargs": [ {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, ] }) prompt_completion_dataset = prompt_completion_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: prompt_completion_dataset.push_to_hub(repo_id, config_name="prompt_completion") preference_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "chosen": [ [{"role": "assistant", "thinking": "Beauty improves clarity and appeal.", "content": "Beautiful."}], [{"role": "assistant", "thinking": "Clarity avoids confusion.", "content": "Explicit."}], [{"role": "assistant", "thinking": "Simplicity is easier to manage.", "content": "Simple."}], [{"role": "assistant", "thinking": "Complexity has structure; complication doesn’t.", "content": "Complex."}], [{"role": "assistant", "thinking": "Flat structures are easier to read.", "content": "Flat."}], [{"role": "assistant", "thinking": "Spacing aids understanding.", "content": "Sparse."}], [{"role": "assistant", "thinking": "Readable code lasts longer.", "content": "Readability."}], [{"role": "assistant", "thinking": "Consistency is more valuable.", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "thinking": "Real-world needs outweigh ideals.", "content": "Practicality."}], [{"role": "assistant", "thinking": "Silent errors cause hidden issues.", "content": "Errors."}], [{"role": "assistant", "thinking": "Silence must be intentional.", "content": "When explicitly silenced."}], [{"role": "assistant", "thinking": "Guessing leads to wrong choices.", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "thinking": "One way reduces confusion.", "content": "One, and preferably only one."}], [{"role": "assistant", "thinking": "A playful nod to Python’s creator.", "content": "Dutch."}], [{"role": "assistant", "thinking": "Action is better than endless delay.", "content": "Now is better than never."}], [{"role": "assistant", "thinking": "Rushed action can be worse.", "content": "Yes, often."}], [{"role": "assistant", "thinking": "Good ideas should be clear.", "content": "It means it's a bad idea."}], [{"role": "assistant", "thinking": "Clarity suggests soundness.", "content": "It means it may be a good idea."}], [{"role": "assistant", "thinking": "Namespaces prevent conflicts.", "content": "Namespaces are one honking great idea."}], ], "rejected": [ [{"role": "assistant", "thinking": "This comparison is nonsensical.", "content": "Better than the moon."}], [{"role": "assistant", "thinking": "This dismisses the value of clarity.", "content": "Worse than nothing."}], [{"role": "assistant", "thinking": "This mixes code style with leisure.", "content": "Better than a long vacation."}], [{"role": "assistant", "thinking": "This overstates complexity as a universal solution.", "content": "Always the answer."}], [{"role": "assistant", "thinking": "This swaps a structural concept for a random object.", "content": "Better than chocolate."}], [{"role": "assistant", "thinking": "This ignores the need for context in sparse designs.", "content": "Without any context."}], [{"role": "assistant", "thinking": "This implies readability is optional, which it is not.", "content": "Optional."}], [{"role": "assistant", "thinking": "This exaggerates special cases into fantasy.", "content": "Enough to become unicorns."}], [{"role": "assistant", "thinking": "This twists the original contrast between practicality and purity.", "content": "Beats reality."}], [{"role": "assistant", "thinking": "This misapplies \"passing\" to a literal driving test.", "content": "Pass their driving test."}], [{"role": "assistant", "thinking": "This suggests forgetting rather than intentional silence.", "content": "Forgotten."}], [{"role": "assistant", "thinking": "This replaces careful judgment with a joke.", "content": "Refuse the opportunity to laugh."}], [{"role": "assistant", "thinking": "This encourages multiple confusing approaches instead of one clear way.", "content": "Two or more confusing methods."}], [{"role": "assistant", "thinking": "This turns a simple example into time-travel absurdity.", "content": "A time traveler."}], [{"role": "assistant", "thinking": "This denies the value of timely action.", "content": "Never better."}], [{"role": "assistant", "thinking": "This removes the sense of tradeoff and possibility.", "content": "Not even a possibility."}], [{"role": "assistant", "thinking": "This inverts the meaning of explainability.", "content": "Clearly the best choice."}], [{"role": "assistant", "thinking": "This treats clarity as something mystical rather than practical.", "content": "Probably magic."}], [{"role": "assistant", "thinking": "This turns a design principle into a silly metaphor.", "content": "Watermelon -- let's plant some!"}], ], "chat_template_kwargs": [ {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "medium", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "high", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, {"reasoning_effort": "low", "model_identity": "You are Tiny ChatGPT, a tiny language model."}, ], }) preference_dataset = preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: preference_dataset.push_to_hub(repo_id, config_name="preference") # fmt: on if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] main(script_args.test_size, script_args.push_to_hub, script_args.repo_id) ================================================ FILE: scripts/generate_tiny_models.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This script generates tiny models used in the TRL library for unit tests. It pushes them to the Hub under the # `trl-internal-testing` organization. # This script is meant to be run when adding new tiny model to the TRL library. import torch from huggingface_hub import HfApi, ModelCard from peft import LoraConfig, get_peft_model from torch import nn from transformers import ( AutoConfig, AutoProcessor, AutoTokenizer, BartModel, Cohere2Config, Cohere2ForCausalLM, CohereConfig, CohereForCausalLM, DeepseekV3Config, DeepseekV3ForCausalLM, FalconMambaConfig, FalconMambaForCausalLM, Gemma2Config, Gemma2ForCausalLM, Gemma3ForConditionalGeneration, GemmaConfig, GemmaForCausalLM, GenerationConfig, Glm4MoeConfig, Glm4MoeForCausalLM, GPT2Config, GPT2LMHeadModel, GPTNeoXConfig, GPTNeoXForCausalLM, GPTNeoXForSequenceClassification, GptOssConfig, GptOssForCausalLM, Idefics2Config, Idefics2ForConditionalGeneration, Idefics3ForConditionalGeneration, InternVLForConditionalGeneration, LlamaConfig, LlamaForCausalLM, LlamaForSequenceClassification, LlavaForConditionalGeneration, LlavaNextForConditionalGeneration, MistralConfig, MistralForCausalLM, OPTConfig, OPTForCausalLM, PaliGemmaForConditionalGeneration, Phi3Config, Phi3ForCausalLM, Qwen2_5_VLConfig, Qwen2_5_VLForConditionalGeneration, Qwen2Config, Qwen2ForCausalLM, Qwen2ForSequenceClassification, Qwen2VLConfig, Qwen2VLForConditionalGeneration, Qwen3_5Config, Qwen3_5ForConditionalGeneration, Qwen3Config, Qwen3ForCausalLM, Qwen3ForSequenceClassification, Qwen3MoeConfig, Qwen3MoeForCausalLM, Qwen3MoeForSequenceClassification, Qwen3VLConfig, Qwen3VLForConditionalGeneration, SmolVLMForConditionalGeneration, T5ForConditionalGeneration, ) ORGANIZATION = "trl-internal-testing" MODEL_CARD = """ --- library_name: transformers tags: [trl] --- # Tiny {model_class_name} This is a minimal model built for unit tests in the [TRL](https://github.com/huggingface/trl) library. """ api = HfApi() def push_to_hub(model, tokenizer, generation_config, prefix=None, suffix=None, force=False): model_class_name = model.__class__.__name__ content = MODEL_CARD.format(model_class_name=model_class_name) model_card = ModelCard(content) if prefix is not None: model_class_name = f"{prefix}-{model_class_name}" repo_id = f"{ORGANIZATION}/{model_class_name}" if suffix is not None: repo_id += f"-{suffix}" if api.repo_exists(repo_id) and not force: print(f"Model {repo_id} already exists, skipping") else: model.push_to_hub(repo_id) model_card.push_to_hub(repo_id) if tokenizer is not None: tokenizer.push_to_hub(repo_id) if generation_config is not None: generation_config.push_to_hub(repo_id) def init_weights_tiny_model(model): """ Initialize tiny test models to avoid NaNs from uninitialized weights. Uses safe defaults: - Linear/Conv1d: Xavier uniform (weights), zero (biases) - Embedding: Normal(0, 0.02) - LayerNorm: Ones (weights), zero (biases) Args: model: PyTorch model (modified in-place) """ for module in model.modules(): if isinstance(module, nn.Linear): # Attention/MLP projections → Xavier or Normal if module.bias is not None: nn.init.zeros_(module.bias) nn.init.xavier_uniform_(module.weight) elif isinstance(module, nn.Embedding): # Token embeddings → GPT-style Normal nn.init.normal_(module.weight, mean=0.0, std=0.02) elif isinstance(module, nn.LayerNorm): # LayerNorm weights always 1, bias 0 nn.init.ones_(module.weight) if module.bias is not None: nn.init.zeros_(module.bias) elif isinstance(module, nn.Conv1d): # Convolutional layers → Xavier or Normal if module.bias is not None: nn.init.zeros_(module.bias) nn.init.xavier_uniform_(module.weight) # Decoder models for model_id, config_class, model_class, dtype, suffix in [ # ("bigscience/bloomz-560m", BloomConfig, BloomForCausalLM, None), # loading fails with this model, see https://huggingface.co/bigscience/bloomz-560m/discussions/14 ("CohereLabs/aya-expanse-8b", CohereConfig, CohereForCausalLM, torch.float16, None), ("CohereLabs/tiny-aya-earth", Cohere2Config, Cohere2ForCausalLM, torch.bfloat16, None), ("deepseek-ai/DeepSeek-R1", DeepseekV3Config, DeepseekV3ForCausalLM, torch.bfloat16, None), # It's important to have R1-0528 as it doesn't have the same chat template ("deepseek-ai/DeepSeek-R1-0528", DeepseekV3Config, DeepseekV3ForCausalLM, torch.bfloat16, "0528"), ("tiiuae/falcon-7b-instruct", FalconMambaConfig, FalconMambaForCausalLM, torch.bfloat16, None), ("google/gemma-2-2b-it", Gemma2Config, Gemma2ForCausalLM, torch.bfloat16, None), ("google/gemma-7b-it", GemmaConfig, GemmaForCausalLM, torch.bfloat16, None), ("openai-community/gpt2", GPT2Config, GPT2LMHeadModel, torch.float32, None), ("EleutherAI/pythia-14m", GPTNeoXConfig, GPTNeoXForCausalLM, torch.float16, None), ("meta-llama/Meta-Llama-3-8B-Instruct", LlamaConfig, LlamaForCausalLM, torch.bfloat16, "3"), ("meta-llama/Llama-3.1-8B-Instruct", LlamaConfig, LlamaForCausalLM, torch.bfloat16, "3.1"), ("meta-llama/Llama-3.2-1B-Instruct", LlamaConfig, LlamaForCausalLM, torch.bfloat16, "3.2"), ("mistralai/Mistral-7B-Instruct-v0.1", MistralConfig, MistralForCausalLM, torch.bfloat16, "0.1"), ("mistralai/Mistral-7B-Instruct-v0.2", MistralConfig, MistralForCausalLM, torch.bfloat16, "0.2"), ("facebook/opt-1.3b", OPTConfig, OPTForCausalLM, torch.float16, None), ("microsoft/Phi-3.5-mini-instruct", Phi3Config, Phi3ForCausalLM, torch.bfloat16, None), ("Qwen/Qwen2.5-32B-Instruct", Qwen2Config, Qwen2ForCausalLM, torch.bfloat16, "2.5"), ("Qwen/Qwen2.5-Coder-0.5B", Qwen2Config, Qwen2ForCausalLM, torch.bfloat16, "2.5-Coder"), ("Qwen/Qwen3-8B", Qwen3Config, Qwen3ForCausalLM, torch.bfloat16, None), ]: revision = "refs/pr/14" if model_id == "Qwen/Qwen3-8B" else "main" # chat template with {% generation %} tokenizer = AutoTokenizer.from_pretrained(model_id, revision=revision) generation_config = GenerationConfig.from_pretrained(model_id, revision=revision) config = config_class( vocab_size=len(tokenizer.vocab), hidden_size=8, num_attention_heads=4, num_key_value_heads=2, num_hidden_layers=2, intermediate_size=32, ) model = model_class(config).to(dtype=dtype) init_weights_tiny_model(model) push_to_hub(model, tokenizer, generation_config, "tiny", suffix) # MoE models for model_id, config_class, model_class, dtype, suffix in [ ("Qwen/Qwen3-30B-A3B", Qwen3MoeConfig, Qwen3MoeForCausalLM, torch.bfloat16, None), ("openai/gpt-oss-20b", GptOssConfig, GptOssForCausalLM, torch.bfloat16, None), ("zai-org/GLM-4.5", Glm4MoeConfig, Glm4MoeForCausalLM, torch.bfloat16, None), ]: tokenizer = AutoTokenizer.from_pretrained(model_id) generation_config = GenerationConfig.from_pretrained(model_id) kwargs = {} if model_id == "zai-org/GLM-4.5": kwargs["n_routed_experts"] = 4 elif model_id in ("Qwen/Qwen3-30B-A3B", "openai/gpt-oss-20b"): kwargs["num_experts"] = 4 config = config_class( vocab_size=len(tokenizer.vocab), hidden_size=8, num_attention_heads=4, num_key_value_heads=2, num_hidden_layers=2, intermediate_size=32, num_experts_per_tok=2, **kwargs, ) model = model_class(config).to(dtype=dtype) init_weights_tiny_model(model) push_to_hub(model, tokenizer, generation_config, "tiny", suffix) # Two slightly bigger models, required for vLLM testing tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-32B-Instruct") generation_config = GenerationConfig.from_pretrained("Qwen/Qwen2.5-32B-Instruct") config = Qwen2Config( vocab_size=len(tokenizer.vocab), hidden_size=128, # increase hidden size so that hidden_size // num_attention_heads = 32, required for vLLM num_attention_heads=4, num_key_value_heads=2, num_hidden_layers=2, intermediate_size=32, ) model = Qwen2ForCausalLM(config).to(dtype=torch.bfloat16) push_to_hub(model, tokenizer, generation_config, "small", "2.5") tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B") generation_config = GenerationConfig.from_pretrained("Qwen/Qwen3-4B") config = Qwen3Config( vocab_size=len(tokenizer.vocab), hidden_size=128, # increase hidden size so that hidden_size // num_attention_heads = 32, required for vLLM num_attention_heads=4, num_key_value_heads=2, num_hidden_layers=2, intermediate_size=32, ) model = Qwen3ForCausalLM(config).to(dtype=torch.bfloat16) push_to_hub(model, tokenizer, generation_config, "small") # Reward models for model_id, model_class, dtype, suffix in [ ("EleutherAI/pythia-14m", GPTNeoXForSequenceClassification, torch.bfloat16, None), ("meta-llama/Llama-3.2-1B-Instruct", LlamaForSequenceClassification, torch.bfloat16, "3.2"), ("Qwen/Qwen2.5-32B-Instruct", Qwen2ForSequenceClassification, torch.bfloat16, "2.5"), ("Qwen/Qwen3-4B", Qwen3ForSequenceClassification, torch.bfloat16, None), ]: tokenizer = AutoTokenizer.from_pretrained(model_id) generation_config = GenerationConfig.from_pretrained(model_id) kwargs = { "num_labels": 1, "hidden_size": 16, "num_attention_heads": 4, "num_key_value_heads": 2, "num_hidden_layers": 2, "intermediate_size": 32, } config = AutoConfig.from_pretrained(model_id, **kwargs) # Bug in transformers: it ignores num_hidden_layers to build layer_types if model_id in ("Qwen/Qwen2.5-32B-Instruct", "Qwen/Qwen3-4B"): config.layer_types = config.layer_types[:2] model = model_class(config).to(dtype=dtype) init_weights_tiny_model(model) push_to_hub(model, tokenizer, generation_config, "tiny", suffix) # MoE Reward models for model_id, model_class, dtype, suffix in [ ("Qwen/Qwen3-30B-A3B", Qwen3MoeForSequenceClassification, torch.bfloat16, None), ]: tokenizer = AutoTokenizer.from_pretrained(model_id) generation_config = GenerationConfig.from_pretrained(model_id) kwargs = { "num_labels": 1, "hidden_size": 16, "num_attention_heads": 4, "num_key_value_heads": 2, "num_hidden_layers": 2, "intermediate_size": 32, "num_experts": 4, "num_experts_per_tok": 2, } config = AutoConfig.from_pretrained(model_id, **kwargs) model = model_class(config).to(dtype=dtype) push_to_hub(model, tokenizer, generation_config, "tiny", suffix) # Encoder-decoder models for model_id, model_class, dtype, suffix in [ ("facebook/bart-base", BartModel, torch.float32, None), ("google/flan-t5-small", T5ForConditionalGeneration, torch.float32, None), ]: tokenizer = AutoTokenizer.from_pretrained(model_id) generation_config = GenerationConfig.from_pretrained(model_id) if model_id != "facebook/bart-base" else None config = AutoConfig.from_pretrained(model_id) config.d_model = 24 model = model_class(config).to(dtype=dtype) push_to_hub(model, tokenizer, generation_config, "tiny", suffix) # Vision Language Models for model_id, model_class, dtype in [ ("google/gemma-3-4b-it", Gemma3ForConditionalGeneration, torch.bfloat16), ("google/paligemma-3b-pt-224", PaliGemmaForConditionalGeneration, torch.float32), ("HuggingFaceM4/idefics2-8b", Idefics2ForConditionalGeneration, torch.float32), ("HuggingFaceM4/Idefics3-8B-Llama3", Idefics3ForConditionalGeneration, torch.bfloat16), ("HuggingFaceTB/SmolVLM2-2.2B-Instruct", SmolVLMForConditionalGeneration, torch.float32), ("llava-hf/llava-1.5-7b-hf", LlavaForConditionalGeneration, torch.float16), # Original model dtype is float16, but it triggers CUDA device side assert error (see GH-4741): ("llava-hf/llava-v1.6-mistral-7b-hf", LlavaNextForConditionalGeneration, torch.bfloat16), ("OpenGVLab/InternVL3-8B-hf", InternVLForConditionalGeneration, torch.bfloat16), ("Qwen/Qwen2-VL-2B-Instruct", Qwen2VLForConditionalGeneration, torch.bfloat16), ("Qwen/Qwen2.5-VL-3B-Instruct", Qwen2_5_VLForConditionalGeneration, torch.bfloat16), ("Qwen/Qwen3-VL-2B-Instruct", Qwen3VLForConditionalGeneration, torch.bfloat16), ("Qwen/Qwen3.5-0.8B", Qwen3_5ForConditionalGeneration, torch.bfloat16), ]: processor = AutoProcessor.from_pretrained(model_id) generation_config = GenerationConfig.from_pretrained(model_id) if model_id != "Qwen/Qwen3.5-0.8B" else None text_config = { "num_hidden_layers": 2, "hidden_size": 16, "num_attention_heads": 4, "num_key_value_heads": 2, "layer_types": None, # Set it automatically from num_hidden_layers } vision_config = { "num_hidden_layers": 2, "hidden_size": 16, "num_attention_heads": 4, "num_key_value_heads": 2, "embed_dim": 64, } kwargs = {} if issubclass(model_class.config_class, (Qwen2VLConfig, Qwen2_5_VLConfig)): text_config["rope_scaling"] = {"type": "default", "mrope_section": [1, 1], "rope_type": "default"} vision_config["depth"] = 2 # Different dict object from text_config; see GH-4101 and transformers#41020 kwargs["rope_scaling"] = {"type": "default", "mrope_section": [1, 1], "rope_type": "default"} if issubclass(model_class.config_class, Qwen2_5_VLConfig): vision_config["out_hidden_size"] = 16 # Different dict object at the config root; see GH-4101 and transformers#41020 kwargs["num_hidden_layers"] = 2 kwargs["hidden_size"] = 16 kwargs["num_attention_heads"] = 4 if issubclass(model_class.config_class, Idefics2Config): kwargs["perceiver_config"] = {"hidden_size": 16} if issubclass(model_class.config_class, Qwen3VLConfig): # So hasattr(config, "layer_types") is False # See: https://github.com/huggingface/transformers/blob/fe5ca9ddaa07fac2872407e75c7a7661216ac956/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L420 del text_config["layer_types"] # "mrope_section" needs 3 elements: for dim, offset in enumerate((1, 2), start=1): mrope_section[dim] # See: https://github.com/huggingface/transformers/blob/fe5ca9ddaa07fac2872407e75c7a7661216ac956/src/transformers/models/qwen3_vl/modeling_qwen3_vl.py#L361 text_config["rope_scaling"] = {"mrope_interleaved": True, "mrope_section": [2, 2, 2], "rope_type": "default"} vision_config["depth"] = 2 vision_config["out_hidden_size"] = 16 if issubclass(model_class.config_class, Qwen3_5Config): # For tiny layer counts, default `layer_types` can end up with no full-attention layers (e.g. 2 layers and # default interval 4), which breaks Qwen3.5 dynamic cache logic. Keep one full-attention layer at the end. text_config["layer_types"] = ["linear_attention", "full_attention"] text_config["full_attention_interval"] = 2 # Qwen3.5-VL vision config expects `depth`/`num_heads`, not `num_hidden_layers`/`num_attention_heads`. vision_config.pop("num_hidden_layers", None) vision_config.pop("num_attention_heads", None) vision_config.pop("num_key_value_heads", None) vision_config.pop("embed_dim", None) vision_config["depth"] = 2 vision_config["num_heads"] = 4 vision_config["intermediate_size"] = 32 vision_config["out_hidden_size"] = 16 if model_id == "llava-hf/llava-v1.6-mistral-7b-hf": # Hotfix: llava-hf/llava-v1.6-mistral-7b-hf mistakesly sets text_config.dtype to "bfloat16". # See https://huggingface.co/llava-hf/llava-v1.6-mistral-7b-hf/discussions/46 text_config["dtype"] = None config = AutoConfig.from_pretrained(model_id, text_config=text_config, vision_config=vision_config, **kwargs) model = model_class(config).to(dtype=dtype) if issubclass(model_class.config_class, Qwen3_5Config): # Qwen3.5 models has some weights in float32, to mirror this in the tiny model we need to convert them to float32 manually. for layer in model.model.language_model.layers: if hasattr(layer, "linear_attn"): # applies to linear attention layers only layer.linear_attn.A_log.data = layer.linear_attn.A_log.data.float() layer.linear_attn.norm.weight.data = layer.linear_attn.norm.weight.data.float() push_to_hub(model, processor, generation_config, "tiny") # PEFT models model = Qwen3ForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen3ForCausalLM", dtype="auto") model = get_peft_model(model, LoraConfig()) generation_config = GenerationConfig.from_pretrained("trl-internal-testing/tiny-Qwen3ForCausalLM") push_to_hub(model, None, None, "tiny") # Same model, but different weights model = Qwen3ForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen3ForCausalLM", dtype="auto") model = get_peft_model(model, LoraConfig()) generation_config = GenerationConfig.from_pretrained("trl-internal-testing/tiny-Qwen3ForCausalLM") push_to_hub(model, None, None, "tiny", "2") ================================================ FILE: scripts/generate_toolcall_dataset.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from dataclasses import dataclass, field from datasets import Dataset from transformers import HfArgumentParser from transformers.utils import get_json_schema @dataclass class ScriptArguments: r""" Arguments for the script. Args: test_size (`float`, *optional*, defaults to `0.1`): Fraction of the dataset to include in the test split. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-internal-testing/zen"`): Hugging Face repository ID to push the dataset to. """ test_size: float = field( default=0.1, metadata={"help": "Fraction of the dataset to include in the test split."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-internal-testing/toolcall", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) def main(test_size, push_to_hub, repo_id): # Fictitious functions to simulate tool calls def start_timer(duration: int) -> int: """ Starts a timer for the specified duration in seconds. Args: duration: Duration in seconds to set the timer for. Returns: The duration set for the timer. """ return duration def get_current_time(location: str) -> str: """ Returns the current time in the specified location. Args: location: The location for which to get the current time. Returns: The current time in the specified location. """ return "06:22:48" def get_air_quality_index(location: str) -> int: """ Returns the air quality index for the specified location. Args: location: The location for which to get the air quality index. Returns: The air quality index for the specified location. """ return 53 def play_music(title: str, artist: str) -> dict: """ Plays music by the specified title and artist. Args: title: The title of the music to play. artist: The artist of the music to play. Returns: A dictionary indicating the status of the music playback. """ return {"status": "Playing"} def get_weather_forecast(city: str, date: str) -> dict: """ Returns the weather forecast for the specified city and date. Args: city: The city for which to get the weather forecast. date: The date for which to get the weather forecast. Returns: A dictionary containing the temperature and weather condition. """ return {"temperature": 22, "condition": "partly cloudy"} def control_light(room: str, state: str) -> dict: """ Controls the light in the specified room. Args: room: The room where the light should be controlled. state: The desired state of the light ("on" or "off"). Returns: A dictionary indicating the state of the light. """ return {"state": state} def create_reminder(time: str, note: str) -> str: """ Creates a reminder for the specified time and note. Args: time: The time for the reminder. note: The note for the reminder. Returns: A confirmation message indicating that the reminder has been set. """ return "I'll remind you to call mom at 7 PM." def get_wind_conditions(city: str, unit: str) -> tuple[int, str]: """ Returns the wind conditions for the specified city. Args: city: The city for which to get the wind conditions. unit: The unit of measurement for the wind speed (e.g., "mph"). Returns: A tuple containing the wind speed and direction. """ return 14, "NW" start_timer = get_json_schema(start_timer) get_current_time = get_json_schema(get_current_time) get_air_quality_index = get_json_schema(get_air_quality_index) play_music = get_json_schema(play_music) get_weather_forecast = get_json_schema(get_weather_forecast) control_light = get_json_schema(control_light) create_reminder = get_json_schema(create_reminder) get_wind_conditions = get_json_schema(get_wind_conditions) # fmt: off language_modeling_dataset = Dataset.from_dict({ "messages": [ [ {"role": "user", "content": "Set a timer for 10 minutes."}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "start_timer", "arguments": {"duration": 600}}}]}, {"role": "tool", "name": "start_timer", "content": "600"}, {"role": "assistant", "content": "Timer set for 10 minutes."}, ], [ {"role": "user", "content": "What time is it in Tokyo?"}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_current_time", "arguments": {"location": "Tokyo"}}}]}, {"role": "tool", "name": "get_current_time", "content": "06:22:48"}, {"role": "assistant", "content": "The current time in Tokyo is 06:22 AM."}, ], [ {"role": "user", "content": "Is the air clean today in Lisbon?"}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_air_quality_index", "arguments": {"location": "Lisbon, Portugal"}}}]}, {"role": "tool", "name": "get_air_quality_index", "content": "53"}, {"role": "assistant", "content": "The air quality is moderate."}, ], [ {"role": "user", "content": "Play some music."}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "play_music", "arguments": {"title": "Take Five", "artist": "Dave Brubeck"}}}]}, {"role": "tool", "name": "play_music", "content": "{'status': 'Playing'}"}, {"role": "assistant", "content": "Enjoy the jazz tunes!"}, ], [ {"role": "user", "content": "What's the weather like tomorrow in Berlin?"}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_weather_forecast", "arguments": {"city": "Berlin", "date": "2025-06-16"}}}]}, {"role": "tool", "name": "get_weather_forecast", "content": "{'temperature': 22, 'condition': 'partly cloudy'}"}, {"role": "assistant", "content": "Tomorrow in Berlin will be partly cloudy with a high of 22°C."} ], [ {"role": "user", "content": "Turn on the living room lights."}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "control_light", "arguments": {"room": "living room", "state": "on"}}}]}, {"role": "tool", "name": "control_light", "content": "{'state': 'on'}"}, {"role": "assistant", "content": "The living room lights are now on."} ], [ {"role": "user", "content": "Remind me to call mom at 7 PM."}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "create_reminder", "arguments": {"time": "19:00", "note": "Call mom"}}}]}, {"role": "tool", "name": "create_reminder", "content": "Reminder set"}, {"role": "assistant", "content": "Okay, I'll remind you to call mom at 7 PM."} ], [ {"role": "user", "content": "How strong is the wind in Chicago right now?"}, {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_wind_conditions", "arguments": {"city": "Chicago", "unit": "mph"}}}]}, {"role": "tool", "name": "get_wind_conditions", "content": "(14, 'NW')"}, {"role": "assistant", "content": "The wind in Chicago is blowing at 14 mph from the northwest."} ] ], "tools": [ json.dumps([start_timer, create_reminder]), json.dumps([get_current_time]), json.dumps([get_air_quality_index, get_weather_forecast, get_wind_conditions]), json.dumps([play_music, control_light]), json.dumps([get_weather_forecast, get_wind_conditions]), json.dumps([control_light]), json.dumps([start_timer, create_reminder]), json.dumps([get_weather_forecast, get_wind_conditions]), ] }) language_modeling_dataset = language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: language_modeling_dataset.push_to_hub(repo_id, config_name="language_modeling") preference_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "Set a timer for 10 minutes."}], [{"role": "user", "content": "What time is it in Tokyo?"}], [{"role": "user", "content": "Is the air clean today in Lisbon?"}], [{"role": "user", "content": "Play some music."}], [{"role": "user", "content": "What's the weather like tomorrow in Berlin?"}], [{"role": "user", "content": "Turn on the living room lights."}], [{"role": "user", "content": "Remind me to call mom at 7 PM."}], [{"role": "user", "content": "How strong is the wind in Chicago right now?"}], ], "chosen": [ [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "start_timer", "arguments": {"duration": 600}}}]}, {"role": "tool", "name": "start_timer", "content": "600"}, {"role": "assistant", "content": "Timer set for 10 minutes."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_current_time", "arguments": {"location": "Tokyo"}}}]}, {"role": "tool", "name": "get_current_time", "content": "06:22:48"}, {"role": "assistant", "content": "The current time in Tokyo is 06:22 AM."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_air_quality_index", "arguments": {"location": "Lisbon, Portugal"}}}]}, {"role": "tool", "name": "get_air_quality_index", "content": "53"}, {"role": "assistant", "content": "The air quality is moderate."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "play_music", "arguments": {"title": "Take Five", "artist": "Dave Brubeck"}}}]}, {"role": "tool", "name": "play_music", "content": "{'status': 'Playing'}"}, {"role": "assistant", "content": "Enjoy the jazz tunes!"}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_weather_forecast", "arguments": {"city": "Berlin", "date": "2025-06-16"}}}]}, {"role": "tool", "name": "get_weather_forecast", "content": "{'temperature': 22, 'condition': 'partly cloudy'}"}, {"role": "assistant", "content": "Tomorrow in Berlin will be partly cloudy with a high of 22°C."} ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "control_light", "arguments": {"room": "living room", "state": "on"}}}]}, {"role": "tool", "name": "control_light", "content": "{'state': 'on'}"}, {"role": "assistant", "content": "The living room lights are now on."} ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "create_reminder", "arguments": {"time": "19:00", "note": "Call mom"}}}]}, {"role": "tool", "name": "create_reminder", "content": "Reminder set"}, {"role": "assistant", "content": "Okay, I’ll remind you to call mom at 7 PM."} ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_wind_conditions", "arguments": {"city": "Chicago", "unit": "mph"}}}]}, {"role": "tool", "name": "get_wind_conditions", "content": "(14, 'NW')"}, {"role": "assistant", "content": "The wind in Chicago is blowing at 14 mph from the northwest."} ], ], "rejected": [ [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "start_timer", "arguments": {"duration": 10}}}]}, {"role": "tool", "name": "start_timer", "content": "10"}, {"role": "assistant", "content": "Timer set for 10 seconds."}, ], [ {"role": "assistant", "content": "It is 6:22 AM in Tokyo."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_air_quality_index", "arguments": {"location": "Lisbon"}}}]}, {"role": "tool", "name": "get_air_quality_index", "content": "53"}, {"role": "assistant", "content": "The air quality is great."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "play_music", "arguments": {"title": "Take Five", "artist": "Daft Punk"}}}]}, {"role": "tool", "name": "play_music", "content": "{'status': 'Playing'}"}, {"role": "assistant", "content": "Playing your song."}, ], [ {"role": "assistant", "content": "Tomorrow in Berlin will be hot and sunny."}, ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "control_light", "arguments": {"room": "living room", "state": "off"}}}]}, {"role": "tool", "name": "control_light", "content": "{'state': 'off'}"}, {"role": "assistant", "content": "The living room lights are now off."} ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "create_reminder", "arguments": {"time": "07:00", "note": "Call mom"}}}]}, {"role": "tool", "name": "create_reminder", "content": "Reminder set"}, {"role": "assistant", "content": "Okay, I'll remind you to call mom at 7 AM."} ], [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_weather_forecast", "arguments": {"city": "Chicago", "date": "2025-06-16"}}}]}, {"role": "tool", "name": "get_weather_forecast", "content": "{'temperature': 22, 'condition': 'partly cloudy'}"}, {"role": "assistant", "content": "Tomorrow in Chicago will be partly cloudy with a high of 22°C."} ], ], "tools": [ json.dumps([start_timer]), json.dumps([get_current_time]), json.dumps([get_air_quality_index]), json.dumps([play_music]), json.dumps([get_weather_forecast]), json.dumps([control_light]), json.dumps([create_reminder]), json.dumps([get_wind_conditions]), ], }) preference_dataset = preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: preference_dataset.push_to_hub(repo_id, config_name="preference") # fmt: on if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] main(script_args.test_size, script_args.push_to_hub, script_args.repo_id) ================================================ FILE: scripts/generate_zen_dataset.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from datasets import Dataset from transformers import HfArgumentParser @dataclass class ScriptArguments: r""" Arguments for the script. Args: test_size (`float`, *optional*, defaults to `0.1`): Fraction of the dataset to include in the test split. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-internal-testing/zen"`): Hugging Face repository ID to push the dataset to. """ test_size: float = field( default=0.1, metadata={"help": "Fraction of the dataset to include in the test split."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-internal-testing/zen", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) def main(test_size, push_to_hub, repo_id): # fmt: off standard_language_modeling_dataset = Dataset.from_dict({ "text": [ "Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated.", "Flat is better than nested.", "Sparse is better than dense.", "Readability counts.", "Special cases aren't special enough to break the rules.", "Although practicality beats purity.", "Errors should never pass silently.", "Unless explicitly silenced.", "In the face of ambiguity, refuse the temptation to guess.", "There should be one-- and preferably only one --obvious way to do it.", "Although that way may not be obvious at first unless you're Dutch.", "Now is better than never.", "Although never is often better than *right* now.", "If the implementation is hard to explain, it's a bad idea.", "If the implementation is easy to explain, it may be a good idea.", "Namespaces are one honking great idea -- let's do more of those!", ], }) standard_language_modeling_dataset = standard_language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_language_modeling_dataset.push_to_hub(repo_id, config_name="standard_language_modeling") standard_prompt_only_dataset = Dataset.from_dict({ "prompt": [ "Beautiful is better than", "Explicit is", "Simple is better", "Complex", "Flat is better than", "Sparse is better", "Readability", "Special cases aren't special", "Although practicality beats", "Errors should never", "Unless explicitly", "In the face of ambiguity, refuse", "There should be one-- and preferably", "Although that way may not be obvious at first unless you're", "Now is", "Although never is often", "If the implementation is hard to explain,", "If the implementation is easy", "Namespaces are one honking great", ], }) standard_prompt_only_dataset = standard_prompt_only_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_prompt_only_dataset.push_to_hub(repo_id, config_name="standard_prompt_only") standard_prompt_completion_dataset = Dataset.from_dict({ "prompt": [ "Beautiful is better than", "Explicit is", "Simple is better", "Complex", "Flat is better than", "Sparse is better", "Readability", "Special cases aren't special", "Although practicality beats", "Errors should never", "Unless explicitly", "In the face of ambiguity, refuse", "There should be one-- and preferably", "Although that way may not be obvious at first unless you're", "Now is", "Although never is often", "If the implementation is hard to explain,", "If the implementation is easy", "Namespaces are one honking great", ], "completion": [ " ugly.", " better than implicit.", " than complex.", " is better than complicated.", " nested.", " than dense.", " counts.", " enough to break the rules.", " purity.", " pass silently.", " silenced.", " the temptation to guess.", " only one --obvious way to do it.", " Dutch.", " better than never.", " better than *right* now.", " it's a bad idea.", " to explain, it may be a good idea.", " idea -- let's do more of those!", ], }) standard_prompt_completion_dataset = standard_prompt_completion_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_prompt_completion_dataset.push_to_hub(repo_id, config_name="standard_prompt_completion") standard_preference_dataset = Dataset.from_dict({ "prompt": [ "Beautiful is better than", "Explicit is", "Simple is better", "Complex", "Flat is better than", "Sparse is better", "Readability", "Special cases aren't special", "Although practicality beats", "Errors should never", "Unless explicitly", "In the face of ambiguity, refuse", "There should be one-- and preferably", "Although that way may not be obvious at first unless you're", "Now is", "Although never is often", "If the implementation is hard to explain,", "If the implementation is easy", "Namespaces are one honking great", ], "chosen": [ " ugly.", " better than implicit.", " than complex.", " is better than complicated.", " nested.", " than dense.", " counts.", " enough to break the rules.", " purity.", " pass silently.", " silenced.", " the temptation to guess.", " only one --obvious way to do it.", " Dutch.", " better than never.", " better than *right* now.", " it's a bad idea.", " to explain, it may be a good idea.", " idea -- let's do more of those!", ], "rejected": [ " the moon.", " worse than nothing.", " than a long vacation.", " is always the answer.", " chocolate.", " without any context.", " is optional.", " enough to become unicorns.", " reality.", " pass their driving test.", " forgotten.", " the opportunity to laugh.", " two or more confusing methods.", " a time traveler.", " never better.", " not even a possibility.", " it's clearly the best choice.", " it's probably magic.", " watermelon -- let's plant some!", ], }) standard_preference_dataset = standard_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_preference_dataset.push_to_hub(repo_id, config_name="standard_preference") standard_implicit_prompt_preference_dataset = Dataset.from_dict({ "chosen": [ "Beautiful is better than ugly.", "Explicit is better than implicit.", "Simple is better than complex.", "Complex is better than complicated.", "Flat is better than nested.", "Sparse is better than dense.", "Readability counts.", "Special cases aren't special enough to break the rules.", "Although practicality beats purity.", "Errors should never pass silently.", "Unless explicitly silenced.", "In the face of ambiguity, refuse the temptation to guess.", "There should be one-- and preferably only one --obvious way to do it.", "Although that way may not be obvious at first unless you're Dutch.", "Now is better than never.", "Although never is often better than *right* now.", "If the implementation is hard to explain, it's a bad idea.", "If the implementation is easy to explain, it may be a good idea.", "Namespaces are one honking great idea -- let's do more of those!", ], "rejected": [ "Beautiful is better than the moon.", "Explicit is worse than nothing.", "Simple is better than a long vacation.", "Complex is always the answer.", "Flat is better than chocolate.", "Sparse is better without any context.", "Readability is optional.", "Special cases aren't special enough to become unicorns.", "Although practicality beats reality.", "Errors should never pass their driving test.", "Unless explicitly forgotten.", "In the face of ambiguity, refuse the opportunity to laugh.", "There should be one-- and preferably two or more confusing methods.", "Although that way may not be obvious at first unless you're a time traveler.", "Now is never better.", "Although never is often not even a possibility.", "If the implementation is hard to explain, it's clearly the best choice.", "If the implementation is easy it's probably magic.", "Namespaces are one honking great watermelon -- let's plant some!", ], }) standard_implicit_prompt_preference_dataset = standard_implicit_prompt_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_implicit_prompt_preference_dataset.push_to_hub(repo_id, config_name="standard_implicit_prompt_preference") standard_unpaired_preference_dataset = Dataset.from_dict({ "prompt": [ "Beautiful is better than", "Explicit is", "Simple is better", "Complex", "Flat is better than", "Sparse is better", "Readability", "Special cases aren't special", "Although practicality beats", "Errors should never", "Unless explicitly", "In the face of ambiguity, refuse", "There should be one-- and preferably", "Although that way may not be obvious at first unless you're", "Now is", "Although never is often", "If the implementation is hard to explain,", "If the implementation is easy", "Namespaces are one honking great", ], "completion": [ " ugly.", " worse than nothing.", " than a long vacation.", " is better than complicated.", " nested.", " without any context.", " counts.", " enough to become unicorns.", " purity.", " pass silently.", " forgotten.", " the temptation to guess.", " only one --obvious way to do it.", " a time traveler.", " better than never.", " not even a possibility.", " it's a bad idea.", " it's probably magic.", " watermelon -- let's plant some!", ], "label": [True, False, False, True, True, False, True, False, True, True, False, True, True, False, True, False, True, False, False], }) standard_unpaired_preference_dataset = standard_unpaired_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_unpaired_preference_dataset.push_to_hub(repo_id, config_name="standard_unpaired_preference") standard_stepwise_supervision_dataset = Dataset.from_dict({ "prompt": [ "Beautiful is better than", "Explicit is better than", "Simple is better than", "Complex is better than", "Flat is better than", "Sparse is better than", "Readability counts", "Special cases aren't special enough", "Although practicality beats", "Errors should never pass", "In the face of ambiguity, refuse", "There should be one-- and preferably only one --", "Although that way may not be", "Now is better than", "Never is often better than", "If the implementation is hard to explain, it's", "If the implementation is easy to explain, it", "Namespaces are one", "Although practicality sometimes beats purity,", ], "completions": [ [", let me think...", " ugly."], [", of course,", " implicit.", " because clarity matters."], ["... let's keep it basic,", " complex."], [" when needed,", " complicated."], [" in terms of structure,", " nested."], ["... especially for readability."], [" especially when others read it."], [", unless...", " they follow the rules."], [" some theoretical elegance,", " purity."], [" silently,", " unless explicitly silenced."], [" the temptation to guess."], [" way to do it,", " but sometimes it's not obvious.", " especially when there's more than one possibility."], [" clear at first,", " it will eventually emerge."], [" later."], [" problematic fixes."], [" likely because it's too complicated."], [" might be a good design."], [" of those great ideas,", " that solve many problems."], [" the code should still aim for balance."], ], "labels": [ [False, True], [False, True, False], [False, True], [True, True], [True, False], [True], [False], [True, False], [False, False], [False, False], [True], [True, True, False], [True, True], [False], [True], [False], [False], [True, True], [False] ] }) standard_stepwise_supervision_dataset = standard_stepwise_supervision_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: standard_stepwise_supervision_dataset.push_to_hub(repo_id, config_name="standard_stepwise_supervision") conversational_language_modeling_dataset = Dataset.from_dict({ "messages": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Beautiful."},], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explicit."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Simple."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Complex."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Flat."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Sparse."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Readability."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Practicality."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Errors."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "When explicitly silenced."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "One, and preferably only one."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "Dutch."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Now is better than never."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "Yes, often."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Namespaces are one honking great idea."}], ], }) conversational_language_modeling_dataset = conversational_language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_language_modeling_dataset.push_to_hub(repo_id, config_name="conversational_language_modeling") conversational_prompt_only_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], }) conversational_prompt_only_dataset = conversational_prompt_only_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_only_dataset.push_to_hub(repo_id, config_name="conversational_prompt_only") conversational_prompt_completion_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "completion": [ [{"role": "assistant", "content": "Beautiful."}], [{"role": "assistant", "content": "Explicit."}], [{"role": "assistant", "content": "Simple."}], [{"role": "assistant", "content": "Complex."}], [{"role": "assistant", "content": "Flat."}], [{"role": "assistant", "content": "Sparse."}], [{"role": "assistant", "content": "Readability."}], [{"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "content": "Practicality."}], [{"role": "assistant", "content": "Errors."}], [{"role": "assistant", "content": "When explicitly silenced."}], [{"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "content": "One, and preferably only one."}], [{"role": "assistant", "content": "Dutch."}], [{"role": "assistant", "content": "Now is better than never."}], [{"role": "assistant", "content": "Yes, often."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "assistant", "content": "Namespaces are one honking great idea."}], ], }) conversational_prompt_completion_dataset = conversational_prompt_completion_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_completion_dataset.push_to_hub(repo_id, config_name="conversational_prompt_completion") conversational_preference_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "chosen": [ [{"role": "assistant", "content": "Beautiful."}], [{"role": "assistant", "content": "Explicit."}], [{"role": "assistant", "content": "Simple."}], [{"role": "assistant", "content": "Complex."}], [{"role": "assistant", "content": "Flat."}], [{"role": "assistant", "content": "Sparse."}], [{"role": "assistant", "content": "Readability."}], [{"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "content": "Practicality."}], [{"role": "assistant", "content": "Errors."}], [{"role": "assistant", "content": "When explicitly silenced."}], [{"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "content": "One, and preferably only one."}], [{"role": "assistant", "content": "Dutch."}], [{"role": "assistant", "content": "Now is better than never."}], [{"role": "assistant", "content": "Yes, often."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "rejected": [ [{"role": "assistant", "content": "Acceptable."}], [{"role": "assistant", "content": "Explained."}], [{"role": "assistant", "content": "Very complex."}], [{"role": "assistant", "content": "Very complicated."}], [{"role": "assistant", "content": "Circular."}], [{"role": "assistant", "content": "Heavy."}], [{"role": "assistant", "content": "Looking complicated."}], [{"role": "assistant", "content": "Yes, special cases are special enough to break the rules."}], [{"role": "assistant", "content": "Nothing."}], [{"role": "assistant", "content": "Warnings."}], [{"role": "assistant", "content": "Never."}], [{"role": "assistant", "content": "Give up."}], [{"role": "assistant", "content": "As many as possible."}], [{"role": "assistant", "content": "French."}], [{"role": "assistant", "content": "Some day."}], [{"role": "assistant", "content": "No, never."}], [{"role": "assistant", "content": "It means it's a good idea."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "Recursion."}], ], }) conversational_preference_dataset = conversational_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_preference_dataset.push_to_hub(repo_id, config_name="conversational_preference") conversational_implicit_prompt_preference_dataset = Dataset.from_dict({ "chosen": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Beautiful."}], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explicit."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Simple."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Complex."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Flat."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Sparse."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Readability."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Practicality."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Errors."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "When explicitly silenced."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "One, and preferably only one."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "Dutch."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Now is better than never."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "Yes, often."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "rejected": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Acceptable."}], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explained."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Very complex."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Very complicated."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Circular."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Heavy."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Looking complicated."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "Yes, special cases are special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Nothing."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Warnings."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "Never."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Give up."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "As many as possible."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "French."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Some day."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "No, never."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a good idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Recursion."}], ], }) conversational_implicit_prompt_preference_dataset = conversational_implicit_prompt_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_implicit_prompt_preference_dataset.push_to_hub(repo_id, config_name="conversational_implicit_prompt_preference") conversational_unpaired_preference_dataset = Dataset.from_dict({ "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "completion": [ [{'role': 'assistant', 'content': 'Beautiful.'}], [{'role': 'assistant', 'content': 'Explicit.'}], [{'role': 'assistant', 'content': 'Simple.'}], [{'role': 'assistant', 'content': 'Very complicated.'}], [{'role': 'assistant', 'content': 'Flat.'}], [{'role': 'assistant', 'content': 'Sparse.'}], [{'role': 'assistant', 'content': 'Readability.'}], [{'role': 'assistant', 'content': 'Yes, special cases are special enough to break the rules.'}], [{'role': 'assistant', 'content': 'Practicality.'}], [{'role': 'assistant', 'content': 'Warnings.'}], [{'role': 'assistant', 'content': 'When explicitly silenced.'}], [{'role': 'assistant', 'content': 'Give up.'}], [{'role': 'assistant', 'content': 'One, and preferably only one.'}], [{'role': 'assistant', 'content': 'French.'}], [{'role': 'assistant', 'content': 'Some day.'}], [{'role': 'assistant', 'content': 'Yes, often.'}], [{'role': 'assistant', 'content': "It means it's a bad idea."}], [{'role': 'assistant', 'content': 'It means it may be a good idea.'}], [{'role': 'assistant', 'content': 'Namespaces are one honking great idea.'}], ], "label": [True, True, True, False, True, True, True, False, True, False, True, False, True, False, False, True, True, True, True], }) conversational_unpaired_preference_dataset = conversational_unpaired_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_unpaired_preference_dataset.push_to_hub(repo_id, config_name="conversational_unpaired_preference") # fmt: on if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] main(script_args.test_size, script_args.push_to_hub, script_args.repo_id) ================================================ FILE: scripts/generate_zen_image_dataset.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field import numpy as np from datasets import Dataset, Features, Image, Value from transformers import HfArgumentParser Message = [{"content": Value("string"), "role": Value("string")}] @dataclass class ScriptArguments: r""" Arguments for the script. Args: test_size (`float`, *optional*, defaults to `0.1`): Fraction of the dataset to include in the test split. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-internal-testing/zen-image"`): Hugging Face repository ID to push the dataset to. """ test_size: float = field( default=0.1, metadata={"help": "Fraction of the dataset to include in the test split."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-internal-testing/zen-image", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) def main(test_size, push_to_hub, repo_id): # fmt: off sizes = np.random.randint(32, 64, size=(19, 2)) data = { "messages": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Beautiful."},], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explicit."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Simple."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Complex."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Flat."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Sparse."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Readability."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Practicality."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Errors."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "When explicitly silenced."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "One, and preferably only one."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "Dutch."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Now is better than never."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "Yes, often."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_language_modeling_dataset = Dataset.from_dict(data, features=Features(messages=Message, image=Image())) conversational_language_modeling_dataset = conversational_language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_language_modeling_dataset.push_to_hub(repo_id, config_name="conversational_language_modeling") sizes = np.random.randint(32, 64, size=(19, 2)) data = { "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_prompt_only_dataset = Dataset.from_dict(data, features=Features(prompt=Message, image=Image())) conversational_prompt_only_dataset = conversational_prompt_only_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_only_dataset.push_to_hub(repo_id, config_name="conversational_prompt_only") sizes = np.random.randint(32, 64, size=(19, 2)) data = { "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "completion": [ [{"role": "assistant", "content": "Beautiful."}], [{"role": "assistant", "content": "Explicit."}], [{"role": "assistant", "content": "Simple."}], [{"role": "assistant", "content": "Complex."}], [{"role": "assistant", "content": "Flat."}], [{"role": "assistant", "content": "Sparse."}], [{"role": "assistant", "content": "Readability."}], [{"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "content": "Practicality."}], [{"role": "assistant", "content": "Errors."}], [{"role": "assistant", "content": "When explicitly silenced."}], [{"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "content": "One, and preferably only one."}], [{"role": "assistant", "content": "Dutch."}], [{"role": "assistant", "content": "Now is better than never."}], [{"role": "assistant", "content": "Yes, often."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_prompt_completion_dataset = Dataset.from_dict(data, features=Features(prompt=Message, completion=Message, image=Image())) conversational_prompt_completion_dataset = conversational_prompt_completion_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_completion_dataset.push_to_hub(repo_id, config_name="conversational_prompt_completion") sizes = np.random.randint(32, 64, size=(19, 2)) data = { "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "chosen": [ [{"role": "assistant", "content": "Beautiful."}], [{"role": "assistant", "content": "Explicit."}], [{"role": "assistant", "content": "Simple."}], [{"role": "assistant", "content": "Complex."}], [{"role": "assistant", "content": "Flat."}], [{"role": "assistant", "content": "Sparse."}], [{"role": "assistant", "content": "Readability."}], [{"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "assistant", "content": "Practicality."}], [{"role": "assistant", "content": "Errors."}], [{"role": "assistant", "content": "When explicitly silenced."}], [{"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "assistant", "content": "One, and preferably only one."}], [{"role": "assistant", "content": "Dutch."}], [{"role": "assistant", "content": "Now is better than never."}], [{"role": "assistant", "content": "Yes, often."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "rejected": [ [{"role": "assistant", "content": "Acceptable."}], [{"role": "assistant", "content": "Explained."}], [{"role": "assistant", "content": "Very complex."}], [{"role": "assistant", "content": "Very complicated."}], [{"role": "assistant", "content": "Circular."}], [{"role": "assistant", "content": "Heavy."}], [{"role": "assistant", "content": "Looking complicated."}], [{"role": "assistant", "content": "Yes, special cases are special enough to break the rules."}], [{"role": "assistant", "content": "Nothing."}], [{"role": "assistant", "content": "Warnings."}], [{"role": "assistant", "content": "Never."}], [{"role": "assistant", "content": "Give up."}], [{"role": "assistant", "content": "As many as possible."}], [{"role": "assistant", "content": "French."}], [{"role": "assistant", "content": "Some day."}], [{"role": "assistant", "content": "No, never."}], [{"role": "assistant", "content": "It means it's a good idea."}], [{"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "assistant", "content": "Recursion."}], ], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_preference_dataset = Dataset.from_dict(data, features=Features(prompt=Message, chosen=Message, rejected=Message, image=Image())) conversational_preference_dataset = conversational_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_preference_dataset.push_to_hub(repo_id, config_name="conversational_preference") sizes = np.random.randint(32, 64, size=(19, 2)) data = { "chosen": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Beautiful."}], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explicit."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Simple."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Complex."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Flat."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Sparse."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Readability."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "No, special cases aren't special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Practicality."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Errors."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "When explicitly silenced."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Refuse the temptation to guess."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "One, and preferably only one."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "Dutch."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Now is better than never."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "Yes, often."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it may be a good idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Namespaces are one honking great idea."}], ], "rejected": [ [{"role": "user", "content": "What is better than ugly?"}, {"role": "assistant", "content": "Acceptable."}], [{"role": "user", "content": "What is better than implicit?"}, {"role": "assistant", "content": "Explained."}], [{"role": "user", "content": "What is better than complex?"}, {"role": "assistant", "content": "Very complex."}], [{"role": "user", "content": "What is better than complicated?"}, {"role": "assistant", "content": "Very complicated."}], [{"role": "user", "content": "What is better than nested?"}, {"role": "assistant", "content": "Circular."}], [{"role": "user", "content": "What is better than dense?"}, {"role": "assistant", "content": "Heavy."}], [{"role": "user", "content": "What counts?"}, {"role": "assistant", "content": "Looking complicated."}], [{"role": "user", "content": "Are special cases enough to break the rules?"}, {"role": "assistant", "content": "Yes, special cases are special enough to break the rules."}], [{"role": "user", "content": "What beats purity?"}, {"role": "assistant", "content": "Nothing."}], [{"role": "user", "content": "What should never pass silently?"}, {"role": "assistant", "content": "Warnings."}], [{"role": "user", "content": "When can errors pass silently?"}, {"role": "assistant", "content": "Never."}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}, {"role": "assistant", "content": "Give up."}], [{"role": "user", "content": "How many ways should there be to do it?"}, {"role": "assistant", "content": "As many as possible."}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}, {"role": "assistant", "content": "French."}], [{"role": "user", "content": "What is better than never?"}, {"role": "assistant", "content": "Some day."}], [{"role": "user", "content": "Is never better than *right* now?"}, {"role": "assistant", "content": "No, never."}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}, {"role": "assistant", "content": "It means it's a good idea."}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}, {"role": "assistant", "content": "It means it's a bad idea."}], [{"role": "user", "content": "Any great ideas?"}, {"role": "assistant", "content": "Recursion."}], ], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_implicit_prompt_preference_dataset = Dataset.from_dict(data, features=Features(chosen=Message, rejected=Message, image=Image())) conversational_implicit_prompt_preference_dataset = conversational_implicit_prompt_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_implicit_prompt_preference_dataset.push_to_hub(repo_id, config_name="conversational_implicit_prompt_preference") sizes = np.random.randint(32, 64, size=(19, 2)) data = { "prompt": [ [{"role": "user", "content": "What is better than ugly?"}], [{"role": "user", "content": "What is better than implicit?"}], [{"role": "user", "content": "What is better than complex?"}], [{"role": "user", "content": "What is better than complicated?"}], [{"role": "user", "content": "What is better than nested?"}], [{"role": "user", "content": "What is better than dense?"}], [{"role": "user", "content": "What counts?"}], [{"role": "user", "content": "Are special cases enough to break the rules?"}], [{"role": "user", "content": "What beats purity?"}], [{"role": "user", "content": "What should never pass silently?"}], [{"role": "user", "content": "When can errors pass silently?"}], [{"role": "user", "content": "What should you do in the face of ambiguity?"}], [{"role": "user", "content": "How many ways should there be to do it?"}], [{"role": "user", "content": "For whom may the way not be obvious at first?"}], [{"role": "user", "content": "What is better than never?"}], [{"role": "user", "content": "Is never better than *right* now?"}], [{"role": "user", "content": "What does it mean if the implementation is hard to explain?"}], [{"role": "user", "content": "What does it mean if the implementation is easy to explain?"}], [{"role": "user", "content": "Any great ideas?"}], ], "completion": [ [{'role': 'assistant', 'content': 'Beautiful.'}], [{'role': 'assistant', 'content': 'Explicit.'}], [{'role': 'assistant', 'content': 'Simple.'}], [{'role': 'assistant', 'content': 'Very complicated.'}], [{'role': 'assistant', 'content': 'Flat.'}], [{'role': 'assistant', 'content': 'Sparse.'}], [{'role': 'assistant', 'content': 'Readability.'}], [{'role': 'assistant', 'content': 'Yes, special cases are special enough to break the rules.'}], [{'role': 'assistant', 'content': 'Practicality.'}], [{'role': 'assistant', 'content': 'Warnings.'}], [{'role': 'assistant', 'content': 'When explicitly silenced.'}], [{'role': 'assistant', 'content': 'Give up.'}], [{'role': 'assistant', 'content': 'One, and preferably only one.'}], [{'role': 'assistant', 'content': 'French.'}], [{'role': 'assistant', 'content': 'Some day.'}], [{'role': 'assistant', 'content': 'Yes, often.'}], [{'role': 'assistant', 'content': "It means it's a bad idea."}], [{'role': 'assistant', 'content': 'It means it may be a good idea.'}], [{'role': 'assistant', 'content': 'Namespaces are one honking great idea.'}], ], "label": [True, True, True, False, True, True, True, False, True, False, True, False, True, False, False, True, True, True, True], "image": [np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in sizes], } conversational_unpaired_preference_dataset = Dataset.from_dict(data, features=Features(prompt=Message, completion=Message, label=Value("bool"), image=Image())) conversational_unpaired_preference_dataset = conversational_unpaired_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_unpaired_preference_dataset.push_to_hub(repo_id, config_name="conversational_unpaired_preference") # fmt: on if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] main(script_args.test_size, script_args.push_to_hub, script_args.repo_id) ================================================ FILE: scripts/generate_zen_multi_image_dataset.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field import numpy as np from datasets import Dataset, Features, Image, List, Value from transformers import HfArgumentParser Message = List({"content": List({"text": Value("string"), "type": Value("string")}), "role": Value("string")}) @dataclass class ScriptArguments: r""" Arguments for the script. Args: test_size (`float`, *optional*, defaults to `0.1`): Fraction of the dataset to include in the test split. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the dataset to the Hugging Face Hub. repo_id (`str`, *optional*, defaults to `"trl-internal-testing/zen-multi-image"`): Hugging Face repository ID to push the dataset to. """ test_size: float = field( default=0.1, metadata={"help": "Fraction of the dataset to include in the test split."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the dataset to the Hugging Face Hub."}, ) repo_id: str = field( default="trl-internal-testing/zen-multi-image", metadata={"help": "Hugging Face repository ID to push the dataset to."}, ) def main(test_size, push_to_hub, repo_id): # fmt: off messages = [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than ugly?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Beautiful."}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than implicit?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Explicit."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than complex?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Simple."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What is better than complicated?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Complex."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than nested?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Flat."}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than dense?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Sparse."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What counts?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Readability."}]}], [{"role": "user", "content": [{"type": "text", "text": "Are special cases enough to break the rules?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "No, special cases aren't special enough to break the rules."}]}], [{"role": "user", "content": [{"type": "text", "text": "What beats purity?"}, {"type": "image"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Practicality."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What should never pass silently?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Errors."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "When can errors pass silently?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "When explicitly silenced."}]}], [{"role": "user", "content": [{"type": "text", "text": "What should you do in the face of ambiguity?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Refuse the temptation to guess."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "How many ways should there be to do it?"}, {"type": "image"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "One, and preferably only one."}]}], [{"role": "user", "content": [{"type": "text", "text": "For whom may the way not be obvious at first?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Dutch."}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than never?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Now is better than never."}]}], [{"role": "user", "content": [{"type": "text", "text": "Is"}, {"type": "image"}, {"type": "text", "text": " never better than *right* now?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Yes, often."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What does it mean if the implementation is hard to explain?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "It means it's a bad idea."}]}], [{"role": "user", "content": [{"type": "text", "text": "What does it mean if the implementation is easy to explain?"}, {"type": "image"}]}, {"role": "assistant", "content": [{"type": "text", "text": "It means it may be a good idea."}]}], [{"role": "user", "content": [{"type": "text", "text": "Any great ideas?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Namespaces are one honking great idea."}]}], ] # Create the images number_of_images = [sum(1 for part in row[0]["content"] if part.get("type") == "image") for row in messages] sizes = [np.random.randint(32, 64, size=(num_images, 2)) for num_images in number_of_images] images = [[np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in s] for s in sizes] conversational_language_modeling_dataset = Dataset.from_dict({"messages": messages, "images": images}, features=Features(messages=Message, images=List(Image()))) conversational_language_modeling_dataset = conversational_language_modeling_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_language_modeling_dataset.push_to_hub(repo_id, config_name="conversational_language_modeling") prompt = [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than ugly?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than implicit?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than complex?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What is better than complicated?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than nested?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than dense?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What counts?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Are special cases enough to break the rules?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What beats purity?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What should never pass silently?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "When can errors pass silently?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What should you do in the face of ambiguity?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "How many ways should there be to do it?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "For whom may the way not be obvious at first?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than never?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Is"}, {"type": "image"}, {"type": "text", "text": " never better than *right* now?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What does it mean if the implementation is hard to explain?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What does it mean if the implementation is easy to explain?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Any great ideas?"}]}], ] # Create the images number_of_images = [sum(1 for part in row[0]["content"] if part.get("type") == "image") for row in prompt] sizes = [np.random.randint(32, 64, size=(num_images, 2)) for num_images in number_of_images] images = [[np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in s] for s in sizes] conversational_prompt_only_dataset = Dataset.from_dict({"prompt": prompt, "images": images}, features=Features(prompt=Message, images=List(Image()))) conversational_prompt_only_dataset = conversational_prompt_only_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_only_dataset.push_to_hub(repo_id, config_name="conversational_prompt_only") prompt = [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than ugly?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than implicit?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than complex?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What is better than complicated?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than nested?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than dense?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What counts?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Are special cases enough to break the rules?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What beats purity?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What should never pass silently?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "When can errors pass silently?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What should you do in the face of ambiguity?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "How many ways should there be to do it?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "For whom may the way not be obvious at first?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than never?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Is"}, {"type": "image"}, {"type": "text", "text": " never better than *right* now?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What does it mean if the implementation is hard to explain?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What does it mean if the implementation is easy to explain?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Any great ideas?"}]}], ] completion = [ [{"role": "assistant", "content": [{"type": "text", "text": "Beautiful."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Explicit."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Simple."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Complex."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Flat."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Sparse."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Readability."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "No, special cases aren't special enough to break the rules."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Practicality."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Errors."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "When explicitly silenced."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Refuse the temptation to guess."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "One, and preferably only one."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Dutch."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Now is better than never."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Yes, often."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it's a bad idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it may be a good idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Namespaces are one honking great idea."}]}], ] # Create the images number_of_images = [sum(1 for part in row[0]["content"] if part.get("type") == "image") for row in prompt] sizes = [np.random.randint(32, 64, size=(num_images, 2)) for num_images in number_of_images] images = [[np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in s] for s in sizes] conversational_prompt_completion_dataset = Dataset.from_dict({"prompt": prompt, "completion": completion, "images": images}, features=Features(prompt=Message, completion=Message, images=List(Image()))) conversational_prompt_completion_dataset = conversational_prompt_completion_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_prompt_completion_dataset.push_to_hub(repo_id, config_name="conversational_prompt_completion") prompt = [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than ugly?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than implicit?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than complex?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What is better than complicated?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What is better than nested?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than dense?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What counts?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Are special cases enough to break the rules?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What beats purity?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "image"}, {"type": "text", "text": "What should never pass silently?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "When can errors pass silently?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What should you do in the face of ambiguity?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "How many ways should there be to do it?"}, {"type": "image"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "For whom may the way not be obvious at first?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What is better than never?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Is"}, {"type": "image"}, {"type": "text", "text": " never better than *right* now?"}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What does it mean if the implementation is hard to explain?"}]}], [{"role": "user", "content": [{"type": "text", "text": "What does it mean if the implementation is easy to explain?"}, {"type": "image"}]}], [{"role": "user", "content": [{"type": "text", "text": "Any great ideas?"}]}], ] chosen = [ [{"role": "assistant", "content": [{"type": "text", "text": "Beautiful."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Explicit."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Simple."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Complex."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Flat."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Sparse."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Readability."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "No, special cases aren't special enough to break the rules."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Practicality."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Errors."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "When explicitly silenced."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Refuse the temptation to guess."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "One, and preferably only one."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Dutch."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Now is better than never."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Yes, often."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it's a bad idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it may be a good idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Namespaces are one honking great idea."}]}], ] rejected = [ [{"role": "assistant", "content": [{"type": "text", "text": "Acceptable."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Explained."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Very complex."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Very complicated."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Circular."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Heavy."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Looking complicated."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Yes, special cases are special enough to break the rules."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Nothing."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Warnings."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Never."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Give up."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "As many as possible."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "French."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Some day."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "No, never."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it's a good idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "It means it's a bad idea."}]}], [{"role": "assistant", "content": [{"type": "text", "text": "Recursion."}]}], ] # Create the images number_of_images = [sum(1 for part in row[0]["content"] if part.get("type") == "image") for row in prompt] sizes = [np.random.randint(32, 64, size=(num_images, 2)) for num_images in number_of_images] images = [[np.random.uniform(low=0.0, high=255.0, size=(h, w, 3)).astype(np.uint8) for h, w in s] for s in sizes] conversational_preference_dataset = Dataset.from_dict({"prompt": prompt, "chosen": chosen, "rejected": rejected, "images": images}, features=Features(prompt=Message, chosen=Message, rejected=Message, images=List(Image()))) conversational_preference_dataset = conversational_preference_dataset.train_test_split(test_size=test_size, shuffle=False) if push_to_hub: conversational_preference_dataset.push_to_hub(repo_id, config_name="conversational_preference") # fmt: on if __name__ == "__main__": parser = HfArgumentParser(ScriptArguments) script_args = parser.parse_args_into_dataclasses()[0] main(script_args.test_size, script_args.push_to_hub, script_args.repo_id) ================================================ FILE: scripts/log_reports.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import json import logging import os from datetime import date from pathlib import Path from tabulate import tabulate MAX_LEN_MESSAGE = 2900 # Slack endpoint has a limit of 3001 characters parser = argparse.ArgumentParser() parser.add_argument("--slack_channel_name", default="trl-push-ci") # Set up logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") def process_log_file(log): failed_tests = [] passed_tests = [] section_num_failed = 0 try: with open(log) as f: for line in f: try: data = json.loads(line) test_name = data.get("nodeid", "") duration = f"{data['duration']:.4f}" if "duration" in data else "N/A" outcome = data.get("outcome", "") if test_name: if outcome == "failed": section_num_failed += 1 failed_tests.append([test_name, duration, log.stem.split("_")[0]]) else: passed_tests.append([test_name, duration, log.stem.split("_")[0]]) except json.JSONDecodeError as e: logging.warning(f"Could not decode line in {log}: {e}") except FileNotFoundError as e: logging.error(f"Log file {log} not found: {e}") except Exception as e: logging.error(f"Error processing log file {log}: {e}") return failed_tests, passed_tests, section_num_failed def main(slack_channel_name): group_info = [] total_num_failed = 0 total_empty_files = [] log_files = list(Path().glob("*.log")) if not log_files: logging.info("No log files found.") return for log in log_files: failed, passed, section_num_failed = process_log_file(log) empty_file = not failed and not passed total_num_failed += section_num_failed total_empty_files.append(empty_file) group_info.append([str(log), section_num_failed, failed]) # Clean up log file try: os.remove(log) except OSError as e: logging.warning(f"Could not remove log file {log}: {e}") # Prepare Slack message payload payload = [ { "type": "header", "text": {"type": "plain_text", "text": f"🤗 Results of the {os.environ.get('TEST_TYPE', '')} TRL tests."}, }, ] if total_num_failed > 0: message = "" for name, num_failed, failed_tests in group_info: if num_failed > 0: message += f"*{name}: {num_failed} failed test(s)*\n" failed_table = [ test[0].split("::")[:2] + [test[0].split("::")[-1][:30] + ".."] for test in failed_tests ] message += ( "\n```\n" + tabulate(failed_table, headers=["Test Location", "Test Name"], tablefmt="grid") + "\n```\n" ) if any(total_empty_files): message += f"\n*{name}: Warning! Empty file - check GitHub action job*\n" # Logging logging.info(f"Total failed tests: {total_num_failed}") print(f"### {message}") if len(message) > MAX_LEN_MESSAGE: message = ( f"❌ There are {total_num_failed} failed tests in total! Please check the action results directly." ) payload.append({"type": "section", "text": {"type": "mrkdwn", "text": message}}) payload.append( { "type": "section", "text": {"type": "mrkdwn", "text": "*For more details:*"}, "accessory": { "type": "button", "text": {"type": "plain_text", "text": "Check Action results"}, "url": f"https://github.com/huggingface/trl/actions/runs/{os.environ['GITHUB_RUN_ID']}", }, } ) payload.append( { "type": "context", "elements": [ { "type": "plain_text", "text": f"On Push main {os.environ.get('TEST_TYPE')} results for {date.today()}", } ], } ) # Send to Slack from slack_sdk import WebClient slack_client = WebClient(token=os.environ.get("SLACK_API_TOKEN")) slack_client.chat_postMessage(channel=f"#{slack_channel_name}", text=message, blocks=payload) else: payload.append( { "type": "section", "text": { "type": "plain_text", "text": "✅ No failures! All tests passed successfully.", "emoji": True, }, } ) logging.info("All tests passed. No errors detected.") if __name__ == "__main__": args = parser.parse_args() main(args.slack_channel_name) ================================================ FILE: tests/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: tests/conftest.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gc from functools import wraps import pytest import torch # ============================================================================ # Model Revision Override # ============================================================================ # To test a tiny model PR before merging to main: # 1. Add the full model_id and PR revision to this dict # 2. Commit and push to trigger CI # 3. Once CI is green, merge the tiny model PR on HF Hub # 4. Remove the entry from this dict and commit # # Example: # MODEL_REVISIONS = { # "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5": "refs/pr/3", # "trl-internal-testing/tiny-LlavaForConditionalGeneration": "refs/pr/5", # } # ============================================================================ MODEL_REVISIONS = { # Add model_id: revision mappings here to test PRs } @pytest.fixture(autouse=True) def apply_model_revisions(monkeypatch): """Auto-inject revision parameter for models defined in MODEL_REVISIONS.""" if not MODEL_REVISIONS: return from transformers import PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin def create_classmethod_wrapper(original_classmethod): # Extract the underlying function from the classmethod original_func = original_classmethod.__func__ @wraps(original_func) def wrapper(cls, pretrained_model_name_or_path, *args, **kwargs): # Direct lookup: only inject if model_id is in the override dict if pretrained_model_name_or_path in MODEL_REVISIONS: if "revision" not in kwargs: kwargs["revision"] = MODEL_REVISIONS[pretrained_model_name_or_path] return original_func(cls, pretrained_model_name_or_path, *args, **kwargs) # Re-wrap as classmethod return classmethod(wrapper) # Patch all transformers Auto* classes for cls in [ PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, ]: monkeypatch.setattr(cls, "from_pretrained", create_classmethod_wrapper(cls.from_pretrained)) @pytest.fixture(autouse=True) def cleanup_gpu(): """ Automatically cleanup GPU memory after each test. This fixture helps prevent CUDA out of memory errors when running tests in parallel with pytest-xdist by ensuring models and tensors are properly garbage collected and GPU memory caches are cleared between tests. """ yield # Cleanup after test gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() torch.cuda.synchronize() ================================================ FILE: tests/data/template.jinja ================================================ {%- if tools %} {{- '<|im_start|>system\n' }} {%- if messages[0].role == 'system' %} {{- messages[0].content + '\n\n' }} {%- endif %} {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} {%- for tool in tools %} {{- "\n" }} {{- tool | tojson }} {%- endfor %} {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} {%- else %} {%- if messages[0].role == 'system' %} {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('') and message.content.endswith('')) %} {%- set ns.multi_step_tool = false %} {%- set ns.last_query_index = index %} {%- endif %} {%- endfor %} {%- for message in messages %} {%- if message.content is string %} {%- set content = message.content %} {%- else %} {%- set content = '' %} {%- endif %} {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }} {%- elif message.role == "assistant" %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is string %} {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {%- if loop.index0 > ns.last_query_index %} {%- if loop.last or (not loop.last and reasoning_content) %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- if message.tool_calls %} {%- for tool_call in message.tool_calls %} {%- if (loop.first and content) or (not loop.first) %} {{- '\n' }} {%- endif %} {%- if tool_call.function %} {%- set tool_call = tool_call.function %} {%- endif %} {{- '\n{"name": "' }} {{- tool_call.name }} {{- '", "arguments": ' }} {%- if tool_call.arguments is string %} {{- tool_call.arguments }} {%- else %} {{- tool_call.arguments | tojson }} {%- endif %} {{- '}\n' }} {%- endfor %} {%- endif %} {{- '<|im_end|>\n' }} {%- elif message.role == "tool" %} {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} {{- '<|im_start|>user' }} {%- endif %} {{- '\n\n' }} {{- content }} {{- '\n' }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} {{- '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- if enable_thinking is defined and enable_thinking is false %} {{- '\n\n\n\n' }} {%- endif %} {%- endif %} ================================================ FILE: tests/distributed/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: tests/distributed/data/accelerate_configs/ddp.yaml ================================================ distributed_type: MULTI_GPU num_processes: 2 ================================================ FILE: tests/distributed/data/accelerate_configs/fsdp2.yaml ================================================ distributed_type: FSDP fsdp_config: fsdp_version: 2 num_processes: 2 ================================================ FILE: tests/distributed/data/accelerate_configs/zero2.yaml ================================================ distributed_type: DEEPSPEED deepspeed_config: zero_stage: 2 num_processes: 2 ================================================ FILE: tests/distributed/data/accelerate_configs/zero3.yaml ================================================ distributed_type: DEEPSPEED deepspeed_config: zero_stage: 3 num_processes: 2 ================================================ FILE: tests/distributed/test_distributed.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import subprocess from pathlib import Path import pytest import torch import transformers from packaging.version import Version from ..testing_utils import TrlTestCase, require_torch_multi_accelerator ROOT = Path(__file__).resolve().parents[2] def run_command(command: list[str], env: dict[str, str]) -> None: result = subprocess.run(command, env=env, cwd=ROOT) assert result.returncode == 0 @pytest.fixture def get_config_path(lazy_shared_datadir): def _get_config_path(config_name): return lazy_shared_datadir / "accelerate_configs" / f"{config_name}.yaml" return _get_config_path @require_torch_multi_accelerator class TestDistributed( TrlTestCase ): # pytest.param("zero3", marks=pytest.mark.xfail(reason="ZeRO 3 is currently failing, see #4899")) @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param( "zero3", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), "fsdp2", ], ) def test_sft(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/sft.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "standard_language_modeling", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param( "zero3", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), "fsdp2", ], ) def test_dpo(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/dpo.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "standard_preference", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param( "zero3", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), "fsdp2", ], ) def test_sft_dataset_streaming(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/sft.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "standard_language_modeling", "--dataset_streaming", "--max_steps", "3", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( condition=Version("2.10") <= Version(torch.__version__), reason="ZeRO 2 + PEFT is failing on torch 2.10; see #4884", ), ), pytest.param( "zero3", marks=pytest.mark.xfail( condition=Version("2.10") <= Version(torch.__version__), reason="ZeRO 3 + PEFT is failing on torch 2.10; see #4884", ), ), "fsdp2", ], ) def test_sft_peft(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/sft.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "standard_language_modeling", "--use_peft", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param( "zero3", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), "fsdp2", ], ) def test_reward(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/reward.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "conversational_implicit_prompt_preference", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param("zero3", marks=pytest.mark.xfail(reason="ZeRO 3 is currently failing, see #4899")), "fsdp2", ], ) def test_rloo(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/rloo.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "conversational_prompt_only", "--reward_model_name_or_path", "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", ], os.environ.copy(), ) # fmt: on @pytest.mark.parametrize( "config", [ "ddp", pytest.param( "zero2", marks=pytest.mark.xfail( Version(transformers.__version__) == Version("5.1.0"), reason="Upstream incompatibility: deepspeed and transformers==5.1.0 (see transformers#43780)", ), ), pytest.param("zero3", marks=pytest.mark.xfail(reason="ZeRO 3 is currently failing, see #4899")), "fsdp2", ], ) def test_grpo(self, config, get_config_path): # fmt: off run_command( [ "accelerate", "launch", "--config_file", get_config_path(config), "trl/scripts/grpo.py", "--output_dir", self.tmp_dir, "--model_name_or_path", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "--dataset_name", "trl-internal-testing/zen", "--dataset_config", "conversational_prompt_only", "--reward_model_name_or_path", "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", ], os.environ.copy(), ) # fmt: on ================================================ FILE: tests/experimental/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: tests/experimental/test_async_grpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import itertools import queue import numpy as np import torch from datasets import load_dataset from transformers import AutoTokenizer from trl.experimental.async_grpo import AsyncGRPOConfig, AsyncGRPOTrainer from trl.experimental.async_grpo.async_rollout_worker import RolloutSample from ..testing_utils import TrlTestCase def dummy_reward_func(completions, **kwargs): return [float(hash(c[0]["content"]) % 100) / 100.0 for c in completions] class _StubRolloutWorker: """Minimal rollout worker stub for testing the trainer in isolation.""" def __init__(self, tokenizer, dataset, num_generations: int = 8, samples_per_weight_sync: int = 10): self.rollout_buffer = queue.Queue() self._samples_per_weight_sync = samples_per_weight_sync self._model_version = 0 self._sample_iter = self._make_sample_iter(tokenizer, dataset, num_generations) def _make_sample_iter(self, tokenizer, dataset, num_generations): for row in itertools.cycle(dataset): completions = [ [{"role": "assistant", "content": f"{row['completion'][0]['content']} {idx}"}] for idx in range(num_generations) ] prompt_completions = [row["prompt"] + completion for completion in completions] prompt_ids = tokenizer.apply_chat_template( row["prompt"], tokenize=True, add_generation_prompt=True, return_dict=False ) prompt_completion_ids = tokenizer.apply_chat_template( prompt_completions, tokenize=True, add_generation_prompt=False, return_dict=False ) rewards = np.array(dummy_reward_func(completions)) advantages = (rewards - rewards.mean()) / rewards.std() for idx in range(num_generations): completion_ids = prompt_completion_ids[idx][len(prompt_ids) :] yield RolloutSample( prompt=row["prompt"], completion=completions[idx], input_ids=prompt_ids + completion_ids, completion_mask=[0] * len(prompt_ids) + [1] * len(completion_ids), old_log_probs=[0.0] * len(prompt_ids) + [-0.5] * len(completion_ids), advantage=float(advantages[idx]), model_version=self._model_version, metrics={"reward": float(rewards[idx]), "reward_std": float(rewards.std())}, ) def _fill_queue(self): for _ in range(self._samples_per_weight_sync): self.rollout_buffer.put(next(self._sample_iter)) def start(self): self._fill_queue() def update_model_version(self, version): self._model_version = version self._fill_queue() def stop(self): pass def pause(self): pass def resume(self): pass def send_weights(self, iterator): pass class TestAsyncGRPOTrainer(TrlTestCase): def test_init_minimal(self): # Test that AsyncGRPOTrainer can be instantiated with only model, reward_model and train_dataset model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train") AsyncGRPOTrainer( model=model_id, reward_funcs=dummy_reward_func, train_dataset=dataset, rollout_worker=_StubRolloutWorker(AutoTokenizer.from_pretrained(model_id), dataset, num_generations=3), ) def test_training(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train") training_args = AsyncGRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage vllm_server_timeout=5.0, # short timeout so test fails fast if queue runs dry report_to="none", ) trainer = AsyncGRPOTrainer( model=model_id, reward_funcs=dummy_reward_func, # unused: the stub pre-computes rewards, but the trainer requires this argument args=training_args, train_dataset=dataset, rollout_worker=_StubRolloutWorker(AutoTokenizer.from_pretrained(model_id), dataset, num_generations=3), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." ================================================ FILE: tests/experimental/test_bco_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from functools import partial import pytest import torch from accelerate import Accelerator from datasets import load_dataset from transformers import AutoModel, AutoModelForCausalLM, AutoTokenizer from transformers.utils import is_peft_available from trl.experimental.bco import BCOConfig, BCOTrainer from trl.experimental.bco.bco_trainer import _process_tokens, _tokenize from ..testing_utils import TrlTestCase, require_no_wandb, require_peft, require_sklearn if is_peft_available(): from peft import LoraConfig @pytest.mark.low_priority class TestBCOTrainer(TrlTestCase): @pytest.mark.parametrize( "config_name", [ "standard_preference", "standard_implicit_prompt_preference", "standard_unpaired_preference", "conversational_preference", "conversational_implicit_prompt_preference", "conversational_unpaired_preference", ], ) @require_sklearn def test_train(self, config_name): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", config_name, split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = BCOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param.cpu(), new_param.cpu()) @require_sklearn def test_train_with_precompute(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates precompute_ref_log_probs=True, report_to="none", ) trainer = BCOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param.cpu(), new_param.cpu()) @require_sklearn def test_train_eval(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False eval_strategy="steps", eval_steps=3, report_to="none", ) trainer = BCOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() @require_sklearn def test_init_with_ref_model_is_model(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False report_to="none", ) with pytest.raises(ValueError): BCOTrainer( model=model, ref_model=model, # ref_model can't be the same as model args=training_args, processing_class=tokenizer, train_dataset=dataset, ) @require_sklearn def test_tokenize_and_process_tokens(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False report_to="none", ) trainer = BCOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dataset, ) tokenized_dataset = dataset.map( _tokenize, fn_kwargs={"tokenizer": trainer.processing_class}, batched=True, batch_size=2, ) assert tokenized_dataset["prompt"][:] == dataset["prompt"][:] assert tokenized_dataset["completion"][:] == dataset["completion"][:] assert tokenized_dataset["label"][:] == dataset["label"][:] assert tokenized_dataset["prompt_input_ids"][0] == [46518, 374, 2664, 1091] assert tokenized_dataset["prompt_attention_mask"][0] == [1, 1, 1, 1] assert tokenized_dataset["answer_input_ids"][0] == [27261, 13] assert tokenized_dataset["answer_attention_mask"][0] == [1, 1] fn_kwargs = { "prefix": "", "is_encoder_decoder": trainer.is_encoder_decoder, "tokenizer": trainer.processing_class, "max_length": trainer.max_length, "truncation_mode": trainer.truncation_mode, } processed_dataset = tokenized_dataset.map(_process_tokens, fn_kwargs=fn_kwargs) assert processed_dataset["prompt"][:] == dataset["prompt"][:] assert processed_dataset["completion"][:] == dataset["completion"][:] assert processed_dataset["label"][:] == dataset["label"][:] assert processed_dataset["prompt_input_ids"][0] == [46518, 374, 2664, 1091] assert processed_dataset["prompt_attention_mask"][0] == [1, 1, 1, 1] assert processed_dataset["completion_input_ids"][0] == [46518, 374, 2664, 1091, 27261, 13, 151645] assert processed_dataset["completion_attention_mask"][0] == [1, 1, 1, 1, 1, 1, 1] assert processed_dataset["completion_labels"][0] == [-100, -100, -100, -100, 27261, 13, 151645] @require_sklearn def test_train_without_providing_ref_model(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = BCOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param.cpu(), new_param.cpu()) @require_sklearn def test_train_udm(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) # Get embedding model embedding_model_id = "trl-internal-testing/tiny-BartModel" embedding_model = AutoModel.from_pretrained(embedding_model_id) embedding_tokenizer = AutoTokenizer.from_pretrained(embedding_model_id) def embed_prompt(input_ids, attention_mask, model): outputs = model(input_ids=input_ids, attention_mask=attention_mask) return outputs.last_hidden_state.mean(dim=1) embedding_model = Accelerator().prepare_model(embedding_model) embedding_func = partial(embed_prompt, model=embedding_model) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = BCOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dataset, embedding_func=embedding_func, embedding_tokenizer=embedding_tokenizer, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param.cpu(), new_param.cpu()) @require_sklearn @require_peft def test_train_without_providing_ref_model_with_lora(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, task_type="CAUSAL_LM") tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference", split="train") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = BCOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dataset, peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): if "lora" in n: new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param.cpu(), new_param.cpu()) @require_sklearn @require_no_wandb def test_generate_during_eval_no_wandb(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False eval_strategy="steps", eval_steps=3, generate_during_eval=True, report_to="none", ) with pytest.raises( ValueError, match="`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve.", ): BCOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) @require_sklearn @require_peft def test_lora_train_and_save(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, task_type="CAUSAL_LM") tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False report_to="none", ) trainer = BCOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dataset["train"], peft_config=lora_config, ) # train the model trainer.train() # save peft adapter trainer.save_model() # assert that the model is loaded without giving OSError AutoModelForCausalLM.from_pretrained(self.tmp_dir) @require_sklearn def test_compute_metrics(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") def dummy_compute_metrics(*args, **kwargs): return {"test": 0.0} training_args = BCOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, # warning raised if not set to False eval_strategy="steps", eval_steps=3, report_to="none", ) trainer = BCOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dataset["train"], eval_dataset=dataset["test"], compute_metrics=dummy_compute_metrics, ) trainer.train() assert trainer.state.log_history[-2]["eval_test"] == 0.0 ================================================ FILE: tests/experimental/test_cpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSeq2SeqLM, AutoTokenizer from trl.experimental.cpo import CPOConfig, CPOTrainer from ..testing_utils import TrlTestCase, require_peft class TestCPOTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token # get t5 as seq2seq example: model_id = "trl-internal-testing/tiny-T5ForConditionalGeneration" self.t5_model = AutoModelForSeq2SeqLM.from_pretrained(model_id, dtype="float32") self.t5_tokenizer = AutoTokenizer.from_pretrained(model_id) @pytest.mark.parametrize( "name, loss_type, config_name", [ ("qwen", "sigmoid", "standard_preference"), ("t5", "hinge", "standard_implicit_prompt_preference"), ("qwen", "ipo", "conversational_preference"), ("qwen", "simpo", "standard_preference"), ("t5", "simpo", "standard_implicit_prompt_preference"), ("qwen", "hinge", "conversational_preference"), ], ) def test_cpo_trainer(self, name, loss_type, config_name): training_args = CPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", beta=0.1, loss_type=loss_type, cpo_alpha=1.0, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) if name == "qwen": model = self.model tokenizer = self.tokenizer elif name == "t5": model = self.t5_model tokenizer = self.t5_tokenizer training_args.is_encoder_decoder = True trainer = CPOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) @pytest.mark.parametrize( "config_name", [ "standard_preference", "standard_implicit_prompt_preference", "conversational_preference", "conversational_implicit_prompt_preference", ], ) @require_peft def test_cpo_trainer_with_lora(self, config_name): from peft import LoraConfig lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) training_args = CPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=4, learning_rate=9e-1, eval_strategy="steps", beta=0.1, cpo_alpha=1.0, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = CPOTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): if "lora" in n: new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) def test_compute_metrics(self): dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_preference") def dummy_compute_metrics(*args, **kwargs): return {"test": 0.0} training_args = CPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, remove_unused_columns=False, do_eval=True, eval_strategy="steps", eval_steps=1, per_device_eval_batch_size=2, report_to="none", ) trainer = CPOTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], compute_metrics=dummy_compute_metrics, ) trainer.train() assert trainer.state.log_history[-2]["eval_test"] == 0.0 def test_alphapo_trainer(self): training_args = CPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", beta=0.1, loss_type="alphapo", alpha=0.5, simpo_gamma=0.5, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_preference") trainer = CPOTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: assert not torch.equal(param, new_param) ================================================ FILE: tests/experimental/test_dppo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from trl.experimental.dppo import DPPOConfig, DPPOTrainer from ..testing_utils import TrlTestCase class TestDPPODivergenceMask: """Unit tests for _compute_divergence_mask with synthetic inputs.""" @staticmethod def make_trainer(divergence_type="binary_tv", epsilon=0.2, epsilon_high=0.28): """Create a minimal DPPOTrainer-like object with just the attributes needed for _compute_divergence_mask.""" class Stub: pass stub = Stub() stub.divergence_type = divergence_type stub.epsilon_low = epsilon stub.epsilon_high = epsilon_high return stub @staticmethod def compute_divergence_mask( trainer_stub, current_logps, sampling_logps, advantages, completion_mask, current_topk_logps=None, sampling_topk_logps=None, ): return DPPOTrainer._compute_divergence_mask( trainer_stub, current_logps, sampling_logps, advantages, completion_mask, current_topk_logps=current_topk_logps, sampling_topk_logps=sampling_topk_logps, ) def test_binary_tv_no_masking_within_threshold(self): stub = self.make_trainer("binary_tv", epsilon=0.2, epsilon_high=0.28) # Policies are very close — no tokens should be masked sampling_logps = torch.log(torch.tensor([[0.5, 0.3, 0.7]])) current_logps = torch.log(torch.tensor([[0.51, 0.29, 0.71]])) advantages = torch.tensor([[1.0]]) completion_mask = torch.ones(1, 3) mask = self.compute_divergence_mask(stub, current_logps, sampling_logps, advantages, completion_mask) assert mask.shape == (1, 3) assert (mask == 1.0).all() def test_binary_tv_masks_positive_advantage_high_divergence(self): stub = self.make_trainer("binary_tv", epsilon=0.01, epsilon_high=0.01) # π much higher than μ, positive advantage → should be masked (invalid_pos) sampling_logps = torch.log(torch.tensor([[0.1]])) current_logps = torch.log(torch.tensor([[0.5]])) advantages = torch.tensor([[1.0]]) completion_mask = torch.ones(1, 1) mask = self.compute_divergence_mask(stub, current_logps, sampling_logps, advantages, completion_mask) assert mask.item() == 0.0 def test_binary_tv_masks_negative_advantage_low_divergence(self): stub = self.make_trainer("binary_tv", epsilon=0.01, epsilon_high=0.01) # π much lower than μ, negative advantage → should be masked (invalid_neg) sampling_logps = torch.log(torch.tensor([[0.5]])) current_logps = torch.log(torch.tensor([[0.1]])) advantages = torch.tensor([[-1.0]]) completion_mask = torch.ones(1, 1) mask = self.compute_divergence_mask(stub, current_logps, sampling_logps, advantages, completion_mask) assert mask.item() == 0.0 def test_binary_tv_respects_completion_mask(self): stub = self.make_trainer("binary_tv", epsilon=0.01, epsilon_high=0.01) # Even though divergence is huge, padding tokens stay 0 sampling_logps = torch.log(torch.tensor([[0.1, 0.5]])) current_logps = torch.log(torch.tensor([[0.9, 0.9]])) advantages = torch.tensor([[1.0]]) completion_mask = torch.tensor([[1.0, 0.0]]) mask = self.compute_divergence_mask(stub, current_logps, sampling_logps, advantages, completion_mask) assert mask[0, 1].item() == 0.0 def test_topk_tv_requires_topk_inputs(self): stub = self.make_trainer("topk_tv") B, T, K = 1, 2, 4 sampling_logps = torch.log(torch.full((B, T), 0.3)) current_logps = torch.log(torch.full((B, T), 0.31)) advantages = torch.tensor([[1.0]]) completion_mask = torch.ones(B, T) # Build top-K distributions that are nearly identical topk_probs = torch.softmax(torch.randn(B, T, K), dim=-1) sampling_topk_logps = torch.log(topk_probs) current_topk_logps = torch.log(topk_probs + 0.001) mask = self.compute_divergence_mask( stub, current_logps, sampling_logps, advantages, completion_mask, current_topk_logps=current_topk_logps, sampling_topk_logps=sampling_topk_logps, ) assert mask.shape == (B, T) assert (mask == 1.0).all() @pytest.mark.low_priority class TestDPPOTrainer(TrlTestCase): @pytest.mark.parametrize("divergence_type", ["binary_tv", "binary_kl"]) def test_training_binary(self, divergence_type): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = DPPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, per_device_train_batch_size=3, num_generations=3, max_completion_length=8, divergence_type=divergence_type, report_to="none", ) trainer = DPPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_training_conversational(self, config_name): dataset = load_dataset("trl-internal-testing/zen", config_name, split="train") training_args = DPPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", ) trainer = DPPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None ================================================ FILE: tests/experimental/test_gkd_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import pytest import torch import torch.nn.functional as F from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig from trl.experimental.gkd import GKDConfig, GKDTrainer from ..testing_utils import TrlTestCase, require_liger_kernel class TestGKDTrainerGenerateOnPolicy(TrlTestCase): @classmethod def setup_class(cls): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" cls.device = "cuda" if torch.cuda.is_available() else "cpu" cls.tokenizer = AutoTokenizer.from_pretrained(model_id) cls.tokenizer.pad_token = cls.tokenizer.eos_token cls.model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32").to(cls.device) cls.generation_config = GenerationConfig( max_new_tokens=20, num_return_sequences=1, pad_token_id=cls.tokenizer.pad_token_id, eos_token_id=cls.tokenizer.eos_token_id, ) def test_generate_on_policy_outputs_deterministic(self): prompts = ["Hello, how are you?", "What's the weather like today?"] tokenized_prompts = self.tokenizer(prompts, return_tensors="pt", padding=True) inputs = { "prompts": tokenized_prompts["input_ids"].to(self.device), "prompt_attention_mask": tokenized_prompts["attention_mask"].to(self.device), } # Set temperature to 0 for deterministic output deterministic_generation_config = GenerationConfig( max_new_tokens=30, num_return_sequences=1, pad_token_id=self.tokenizer.pad_token_id, eos_token_id=self.tokenizer.eos_token_id, do_sample=False, temperature=0.0, ) outputs = GKDTrainer.generate_on_policy_outputs( self.model, inputs, deterministic_generation_config, self.tokenizer.pad_token_id ) new_input_ids, new_attention_mask, new_labels = outputs # Decode the generated outputs generated_texts = self.tokenizer.batch_decode(new_input_ids, skip_special_tokens=True) # Check if the generated texts start with the original prompts for prompt, generated_text in zip(prompts, generated_texts, strict=True): assert generated_text.startswith(prompt), ( f"Generated text '{generated_text}' does not start with prompt '{prompt}'" ) # Run the generation twice and check if the outputs are identical outputs2 = GKDTrainer.generate_on_policy_outputs( self.model, inputs, deterministic_generation_config, self.tokenizer.pad_token_id ) new_input_ids2, new_attention_mask2, new_labels2 = outputs2 # Check if the two generations are identical assert torch.all(new_input_ids.eq(new_input_ids2)), "Deterministic generations are not identical" assert torch.all(new_attention_mask.eq(new_attention_mask2)), ( "Attention masks for deterministic generations are not identical" ) assert torch.all(new_labels.eq(new_labels2)), "Labels for deterministic generations are not identical" def test_generate_on_policy_outputs(self): prompts = ["Hello, how are you?", "What's the weather like today?"] tokenized_prompts = self.tokenizer(prompts, return_tensors="pt", padding=True) inputs = { "prompts": tokenized_prompts["input_ids"].to(self.device), "attention_mask": tokenized_prompts["attention_mask"].to(self.device), } outputs = GKDTrainer.generate_on_policy_outputs( self.model, inputs, self.generation_config, self.tokenizer.pad_token_id ) # Check that outputs is a tuple of three tensors assert isinstance(outputs, tuple) assert len(outputs) == 3 new_input_ids, new_attention_mask, new_labels = outputs # Check shapes batch_size = len(prompts) assert new_input_ids.shape[0] == batch_size assert new_attention_mask.shape[0] == batch_size assert new_labels.shape[0] == batch_size # Check types assert isinstance(new_input_ids, torch.Tensor) assert isinstance(new_attention_mask, torch.Tensor) assert isinstance(new_labels, torch.Tensor) # Check that new_input_ids and new_attention_mask have the same shape assert new_input_ids.shape == new_attention_mask.shape assert new_labels.shape == new_attention_mask.shape class TestGeneralizedJSDLoss(TrlTestCase): def setup_method(self): self.batch_size = 2 self.seq_length = 3 self.vocab_size = 5 self.student_logits = torch.randn(self.batch_size, self.seq_length, self.vocab_size) self.teacher_logits = torch.randn(self.batch_size, self.seq_length, self.vocab_size) def test_uniform_distribution(self): logits = torch.ones(1, 1, self.vocab_size) loss = GKDTrainer.generalized_jsd_loss(logits, logits) assert round(abs(loss.item() - 0), 5) == 0 def test_generalized_jsd_loss_edge_cases(self): # Setup student_logits = torch.log(torch.tensor([[0.1, 0.9]])).unsqueeze(0) teacher_logits = torch.log(torch.tensor([[0.9, 0.1]])).unsqueeze(0) # Case 1: beta = 1 (should be equivalent to KL(student || teacher)) loss_beta_1 = GKDTrainer.generalized_jsd_loss(student_logits, teacher_logits, beta=1) expected_loss_beta_1 = F.kl_div( F.log_softmax(teacher_logits, dim=-1), F.softmax(student_logits, dim=-1), reduction="batchmean" ) assert round(abs(loss_beta_1.item() - expected_loss_beta_1.item()), 5) == 0 # Case 2: beta = 0 (should be equivalent to KL(teacher || student)) loss_beta_0 = GKDTrainer.generalized_jsd_loss(student_logits, teacher_logits, beta=0) expected_loss_beta_0 = F.kl_div( F.log_softmax(student_logits, dim=-1), F.softmax(teacher_logits, dim=-1), reduction="batchmean" ) assert round(abs(loss_beta_0.item() - expected_loss_beta_0.item()), 5) == 0 def test_output_shape(self): loss = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits) assert torch.is_tensor(loss) assert loss.shape == torch.Size([]) def test_beta_values(self): loss_beta_0 = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, beta=0) loss_beta_1 = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, beta=1) assert loss_beta_0 != loss_beta_1 def test_temperature_scaling(self): loss_temp_1 = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, temperature=1) loss_temp_2 = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, temperature=2) assert loss_temp_1 != loss_temp_2 def test_reduction_methods(self): loss_batchmean = GKDTrainer.generalized_jsd_loss( self.student_logits, self.teacher_logits, reduction="batchmean" ) loss_sum = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, reduction="sum") loss_mean = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, reduction="mean") loss_none = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, reduction="none") assert loss_batchmean.shape == torch.Size([]) assert loss_sum.shape == torch.Size([]) assert loss_mean.shape == torch.Size([]) assert loss_none.shape == self.student_logits.shape def test_symmetry(self): student_teacher = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, beta=0.1) teacher_student = GKDTrainer.generalized_jsd_loss(self.teacher_logits, self.student_logits, beta=0.1) assert student_teacher != teacher_student student_teacher = GKDTrainer.generalized_jsd_loss(self.student_logits, self.teacher_logits, beta=0.5) teacher_student = GKDTrainer.generalized_jsd_loss(self.teacher_logits, self.student_logits, beta=0.5) assert student_teacher == teacher_student def test_zero_loss_for_identical_inputs(self): identical_logits = torch.randn(self.batch_size, self.seq_length, self.vocab_size) loss = GKDTrainer.generalized_jsd_loss(identical_logits, identical_logits) assert round(abs(loss.item() - 0), 6) == 0 class TestGKDTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.teacher_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token def test_gkd_trainer(self): training_args = GKDConfig( output_dir=self.tmp_dir, dataloader_drop_last=True, eval_strategy="steps", max_steps=4, eval_steps=2, save_steps=2, per_device_train_batch_size=2, per_device_eval_batch_size=2, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling") trainer = GKDTrainer( model=self.model_id, teacher_model=self.model_id, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, ) trainer.train() assert trainer.state.log_history[(-1)]["train_loss"] is not None assert trainer.state.log_history[0]["eval_loss"] is not None assert "model.safetensors" in os.listdir(self.tmp_dir + "/checkpoint-2") @require_liger_kernel @pytest.mark.xfail(reason="Computing the Liger loss spikes GPU memory usage, causing the test to run OOM.") def test_gkd_trainer_with_liger(self): training_args = GKDConfig( output_dir=self.tmp_dir, report_to="none", use_liger_kernel=True, ) dummy_dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling") trainer = GKDTrainer( model=self.model_id, teacher_model=self.model_id, args=training_args, train_dataset=dummy_dataset["train"], processing_class=self.tokenizer, ) # Ensure liger fused JSD path is enabled; if not, skip (runtime may lack system libs) if not getattr(trainer, "use_liger_gkd_loss", False): pytest.skip("Liger fused JSD not enabled at runtime; skipping fused-loss assertion") trainer.train() # Check we logged a train loss assert trainer.state.log_history[-1]["train_loss"] is not None def test_generation_config_init(self): training_args = GKDConfig(output_dir=self.tmp_dir) dummy_dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling") trainer = GKDTrainer( model=self.model_id, teacher_model=self.model_id, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, ) assert trainer.generation_config.pad_token_id == self.tokenizer.eos_token_id assert trainer.generation_config.eos_token_id == self.model.generation_config.eos_token_id assert trainer.generation_config.max_new_tokens == training_args.max_new_tokens assert trainer.generation_config.temperature == training_args.temperature assert trainer.generation_config.top_k == 0 ================================================ FILE: tests/experimental/test_gold_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from types import SimpleNamespace import pytest import torch from datasets import load_dataset from transformers import AutoTokenizer from trl.experimental.gold.gold_trainer import GOLDTrainer, ULDLoss, build_teacher_inputs_from_texts from trl.experimental.utils import DataCollatorForChatML @pytest.fixture(scope="module") def openr1_examples(): try: dataset = load_dataset( "HuggingFaceTB/OpenR1-Math-220k-default-verified", "all", split="train[:3]", ) except Exception as exc: # pragma: no cover - network/environment dependent pytest.skip(f"OpenR1 dataset unavailable: {exc}") return [{"messages": row["messages"]} for row in dataset] @pytest.fixture(scope="module") def countdown_examples(): try: dataset = load_dataset( "HuggingFaceTB/Countdown-Tasks-3to4", "gkd_verified_Qwen2.5-7B-Instruct", split="train[:3]", ) except Exception as exc: # pragma: no cover - network/environment dependent pytest.skip(f"Countdown dataset unavailable: {exc}") return [{"messages": row["messages"]} for row in dataset] def _teacher_inputs_from_collator(student_tok, teacher_tok, batch): prompt_texts = [] completion_texts = [] pad_token_id = student_tok.pad_token_id for prompt_ids_tensor, input_ids_tensor, labels_tensor in zip( batch["prompts"], batch["input_ids"], batch["labels"], strict=True ): prompt_ids = prompt_ids_tensor.tolist() if pad_token_id is not None: prompt_ids = [tok for tok in prompt_ids if tok != pad_token_id] prompt_texts.append(student_tok.decode(prompt_ids, skip_special_tokens=False)) input_ids = input_ids_tensor.tolist() labels = labels_tensor.tolist() completion_token_ids = [tok for tok, label in zip(input_ids, labels, strict=True) if label != -100] completion_texts.append(student_tok.decode(completion_token_ids, skip_special_tokens=False)) teacher_input_ids, teacher_labels, _, _ = build_teacher_inputs_from_texts( teacher_tok, prompt_texts, completion_texts ) return teacher_input_ids, teacher_labels, completion_texts def _assert_alignment_covers_completion(loss_fn, batch, teacher_input_ids, teacher_labels): for idx in range(batch["input_ids"].shape[0]): student_mask = batch["attention_mask"][idx].bool() student_ids = batch["input_ids"][idx][student_mask] student_labels = batch["labels"][idx][student_mask] student_answer_ids = student_ids[student_labels != -100].tolist() teacher_answer_mask = teacher_labels[idx] != -100 teacher_answer_ids = teacher_input_ids[idx][teacher_answer_mask].tolist() student_groups, teacher_groups = loss_fn._build_alignment_groups_from_ids( student_answer_ids, teacher_answer_ids ) assert student_groups, "Student alignment groups must not be empty" assert teacher_groups, "Teacher alignment groups must not be empty" assert sorted(idx for group in student_groups for idx in group) == list(range(len(student_answer_ids))) assert sorted(idx for group in teacher_groups for idx in group) == list(range(len(teacher_answer_ids))) @pytest.mark.slow def test_chatml_collator_preserves_completion_llama(llama_tokenizer, qwen_tokenizer, openr1_examples): collator = DataCollatorForChatML(tokenizer=llama_tokenizer, max_length=512) batch = collator(openr1_examples) assistant_texts = [example["messages"][-1]["content"] for example in openr1_examples] decoded_batch = llama_tokenizer.batch_decode(batch["input_ids"], skip_special_tokens=False) for decoded, assistant in zip(decoded_batch, assistant_texts, strict=True): assert assistant.strip() in decoded teacher_input_ids, teacher_labels, completion_texts = _teacher_inputs_from_collator( llama_tokenizer, qwen_tokenizer, batch ) for completion, assistant in zip(completion_texts, assistant_texts, strict=True): assert assistant.strip() in completion assert completion.strip() config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.6, uld_hybrid_unmatched_weight=0.4, ) loss_fn = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) _assert_alignment_covers_completion(loss_fn, batch, teacher_input_ids, teacher_labels) torch.manual_seed(0) student_vocab = len(llama_tokenizer) teacher_vocab = len(qwen_tokenizer) batch_size, seq_len = batch["input_ids"].shape student_logits = torch.randn(batch_size, seq_len, student_vocab) teacher_logits = torch.randn(batch_size, teacher_input_ids.shape[1], teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=batch["labels"], teacher_labels=teacher_labels, student_input_ids=batch["input_ids"], teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) @pytest.mark.slow def test_chatml_collator_preserves_completion_llama_countdown(llama_tokenizer, qwen_tokenizer, countdown_examples): collator = DataCollatorForChatML(tokenizer=llama_tokenizer, max_length=512) batch = collator(countdown_examples) assistant_texts = [example["messages"][-1]["content"] for example in countdown_examples] decoded_batch = llama_tokenizer.batch_decode(batch["input_ids"], skip_special_tokens=False) for decoded, assistant in zip(decoded_batch, assistant_texts, strict=True): assert assistant.strip() in decoded teacher_input_ids, teacher_labels, completion_texts = _teacher_inputs_from_collator( llama_tokenizer, qwen_tokenizer, batch ) for completion, assistant in zip(completion_texts, assistant_texts, strict=True): assert assistant.strip() in completion assert completion.strip() config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.6, uld_hybrid_unmatched_weight=0.4, ) loss_fn = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) _assert_alignment_covers_completion(loss_fn, batch, teacher_input_ids, teacher_labels) torch.manual_seed(2) student_vocab = len(llama_tokenizer) teacher_vocab = len(qwen_tokenizer) batch_size, seq_len = batch["input_ids"].shape student_logits = torch.randn(batch_size, seq_len, student_vocab) teacher_logits = torch.randn(batch_size, teacher_input_ids.shape[1], teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=batch["labels"], teacher_labels=teacher_labels, student_input_ids=batch["input_ids"], teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) @pytest.mark.slow def test_chatml_collator_preserves_completion_smollm(smollm_tokenizer, qwen_tokenizer, openr1_examples): collator = DataCollatorForChatML(tokenizer=smollm_tokenizer, max_length=512) batch = collator(openr1_examples) assistant_texts = [example["messages"][-1]["content"] for example in openr1_examples] decoded_batch = smollm_tokenizer.batch_decode(batch["input_ids"], skip_special_tokens=False) for decoded, assistant in zip(decoded_batch, assistant_texts, strict=True): assert assistant.strip() in decoded teacher_input_ids, teacher_labels, completion_texts = _teacher_inputs_from_collator( smollm_tokenizer, qwen_tokenizer, batch ) for completion, assistant in zip(completion_texts, assistant_texts, strict=True): assert assistant.strip() in completion assert completion.strip() config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.5, uld_hybrid_unmatched_weight=0.5, ) loss_fn = ULDLoss(config, student_tokenizer=smollm_tokenizer, teacher_tokenizer=qwen_tokenizer) _assert_alignment_covers_completion(loss_fn, batch, teacher_input_ids, teacher_labels) torch.manual_seed(1) student_vocab = len(smollm_tokenizer) teacher_vocab = len(qwen_tokenizer) batch_size, seq_len = batch["input_ids"].shape student_logits = torch.randn(batch_size, seq_len, student_vocab) teacher_logits = torch.randn(batch_size, teacher_input_ids.shape[1], teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=batch["labels"], teacher_labels=teacher_labels, student_input_ids=batch["input_ids"], teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) def build_config(**overrides): base = dict( uld_crossentropy_weight=0.0, uld_distillation_weight=1.0, uld_student_temperature=1.0, uld_teacher_temperature=1.0, uld_skip_student_eos=False, uld_skip_teacher_eos=False, use_extended_uld=True, uld_use_hybrid_loss=False, uld_hybrid_matched_weight=None, uld_hybrid_unmatched_weight=None, beta=0.5, ) base.update(overrides) return SimpleNamespace(**base) @pytest.fixture(scope="session") def llama_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("TinyLlama/TinyLlama-1.1B-Chat-v1.0") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token return tokenizer @pytest.fixture(scope="session") def qwen_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token return tokenizer @pytest.fixture(scope="session") def smollm_tokenizer(): tokenizer = AutoTokenizer.from_pretrained("HuggingFaceTB/SmolLM3-3B") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token return tokenizer def encode_prompt_completion(tokenizer, prompt, completion): prompt_ids = tokenizer(prompt, add_special_tokens=False)["input_ids"] completion_ids = tokenizer(completion, add_special_tokens=False)["input_ids"] eos_id = tokenizer.eos_token_id if eos_id is not None: completion_ids = completion_ids + [eos_id] input_ids = prompt_ids + completion_ids labels = [-100] * len(prompt_ids) + completion_ids return input_ids, labels def pad_tokens(ids, pad_id, target_length): return ids + [pad_id] * (target_length - len(ids)) def pad_labels(labels, target_length): return labels + [-100] * (target_length - len(labels)) def test_process_completions_to_buffer_left_pads_prompt_retokenization(): class DummyBatch: def __init__(self, input_ids): self.input_ids = input_ids def to(self, device): self.input_ids = self.input_ids.to(device) return self class RecordingTokenizer: pad_token_id = 0 pad_token = "" def __init__(self): self.padding_side = "right" self.calls = [] self._prompt_ids = { "short": [11], "longer": [21, 22], } def __call__( self, texts, return_tensors, padding, truncation, max_length, add_special_tokens, padding_side=None, ): assert return_tensors == "pt" assert padding == "longest" assert not truncation assert max_length is None assert not add_special_tokens self.calls.append(padding_side) side = padding_side or self.padding_side encoded = [torch.tensor(self._prompt_ids[text], dtype=torch.long) for text in texts] max_len = max(len(ids) for ids in encoded) padded = [] for ids in encoded: pad_width = max_len - len(ids) if pad_width: pad = torch.full((pad_width,), self.pad_token_id, dtype=torch.long) ids = torch.cat([pad, ids]) if side == "left" else torch.cat([ids, pad]) padded.append(ids) return DummyBatch(torch.stack(padded)) def batch_decode(self, sequences, skip_special_tokens=False, clean_up_tokenization_spaces=False): del skip_special_tokens, clean_up_tokenization_spaces return [" ".join(str(token) for token in sequence) for sequence in sequences] trainer = GOLDTrainer.__new__(GOLDTrainer) trainer.accelerator = SimpleNamespace(device=torch.device("cpu")) trainer.processing_class = RecordingTokenizer() trainer.args = SimpleNamespace(max_length=None) trainer._buffered_inputs = [None] trainer._buffered_text_logs = [None] GOLDTrainer._process_completions_to_buffer( trainer, slices=[{"slice": "original"}], on_policy_indices=[0], local_slice_indices=[0, 0], completion_ids=[[31], [41]], prompts_text=["short", "longer"], prompts_text_with_special=["short", "longer"], max_completion_length=1, ) buffered_inputs = trainer._buffered_inputs[0] assert trainer.processing_class.calls == ["left"] assert trainer.processing_class.padding_side == "right" assert torch.equal(buffered_inputs["input_ids"], torch.tensor([[0, 11, 31], [21, 22, 41]], dtype=torch.long)) assert torch.equal(buffered_inputs["attention_mask"], torch.tensor([[0, 1, 1], [1, 1, 1]], dtype=torch.long)) assert torch.equal(buffered_inputs["labels"], torch.tensor([[-100, -100, 31], [-100, -100, 41]])) def test_alignment_groups_cover_all_tokens(llama_tokenizer, qwen_tokenizer): config = build_config() loss = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) text = "SmolLM3-3B is smaller than Llama 3.2 but still capable." student_ids = llama_tokenizer(text, add_special_tokens=False)["input_ids"] teacher_ids = qwen_tokenizer(text, add_special_tokens=False)["input_ids"] student_groups, teacher_groups = loss._build_alignment_groups_from_ids(student_ids, teacher_ids) assert len(student_groups) == len(teacher_groups) assert sorted(idx for group in student_groups for idx in group) == list(range(len(student_ids))) assert sorted(idx for group in teacher_groups for idx in group) == list(range(len(teacher_ids))) def test_merge_probabilities_multiplies_split_tokens(): config = build_config() # Use simple 3-token vocabulary to validate merging behaviour # probs[0] = P(token | context) at position 0 for all vocab tokens # probs[1] = P(token | context) at position 1 for all vocab tokens probs = torch.tensor([[0.6, 0.3, 0.1], [0.2, 0.5, 0.3]]) loss = ULDLoss(config, student_tokenizer=None, teacher_tokenizer=None) # token_ids[1] = 1 means the actual token at position 1 is token ID 1 # So we should extract P(token_id=1 | ...) = probs[1, 1] = 0.5 token_ids = [0, 1] # Actual generated tokens merged = loss._merge_probabilities_with_alignment_groups(probs, [[0, 1]], token_ids=token_ids) # Expected: P_merged(y) = P(y | context_0) × P(token_1=1 | context_1) # For each vocab token y, multiply marginal prob at pos 0 by scalar conditional prob of actual token at pos 1 expected = probs[0] * probs[1, 1] # probs[1, 1] = 0.5 # Expected unnormalized: [0.6 * 0.5, 0.3 * 0.5, 0.1 * 0.5] = [0.3, 0.15, 0.05] torch.testing.assert_close(merged[0], expected) def test_initialize_vocabulary_mapping_contains_common_tokens(llama_tokenizer, qwen_tokenizer): config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=1.0, uld_hybrid_unmatched_weight=0.0, ) loss = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) common_tokens = ["Hello", "world", "-", "ol", "LM", "3", "B"] for token in common_tokens: student_id = llama_tokenizer.convert_tokens_to_ids(token) teacher_id = qwen_tokenizer.convert_tokens_to_ids(token) assert student_id is not None assert teacher_id is not None assert teacher_id in loss._vocab_mapping assert loss._vocab_mapping[teacher_id] == student_id assert teacher_id in loss._teacher_matched_ids assert student_id in loss._student_matched_ids def test_get_start_and_size_answers_skips_prompt_tokens(): trainer = ULDLoss.__new__(ULDLoss) trainer.ignore_index = -100 answers = torch.tensor( [ [-100, -100, -100, 10, 20, 30, -100, -100], [-100, 5, 6, 7, -100, -100, -100, -100], [-100, -100, -100, -100, -100, -100, -100, -100], ] ) starts, sizes = trainer._get_start_and_size_answers(answers) assert starts == [3, 1, 0] assert sizes == [3, 3, 0] @pytest.mark.slow def test_generate_on_policy_outputs_masks_prompt(llama_tokenizer): trainer = GOLDTrainer.__new__(GOLDTrainer) trainer.use_transformers_paged = False trainer.processing_class = llama_tokenizer prompt_text = "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\nHello?<|eot_id|>" completion_text = "<|start_header_id|>assistant<|end_header_id|>\nHi there!" prompt_ids = llama_tokenizer(prompt_text, add_special_tokens=False)["input_ids"] completion_ids = llama_tokenizer(completion_text, add_special_tokens=False)["input_ids"] pad_id = llama_tokenizer.pad_token_id pad_width = 3 prompt_tensor = torch.full((1, len(prompt_ids) + pad_width), pad_id, dtype=torch.long) prompt_tensor[0, pad_width:] = torch.tensor(prompt_ids, dtype=torch.long) prompt_mask = (prompt_tensor != pad_id).long() # model.generate() returns full sequences including left-padding from the input completion_tensor = torch.tensor(completion_ids, dtype=torch.long).unsqueeze(0) generated_sequence = torch.cat([prompt_tensor, completion_tensor], dim=1) class DummyModel: def generate(self, input_ids, attention_mask, generation_config, return_dict_in_generate): assert torch.equal(input_ids, prompt_tensor) assert torch.equal(attention_mask, prompt_mask) return SimpleNamespace(sequences=generated_sequence) generation_config = SimpleNamespace(max_completion_length=None, temperature=None, top_k=None, top_p=None) new_ids, new_mask, new_labels, prompt_texts, completion_texts = GOLDTrainer.generate_on_policy_outputs( trainer, DummyModel(), {"prompts": prompt_tensor, "prompt_attention_mask": prompt_mask}, generation_config, pad_id, ) assert torch.equal(new_ids, generated_sequence) if pad_id is not None: expected_mask = (generated_sequence != pad_id).long() assert torch.equal(new_mask, expected_mask) else: assert torch.all(new_mask == 1) padded_prompt_len = prompt_tensor.shape[1] assert torch.all(new_labels[0, :padded_prompt_len] == -100) assert torch.equal(new_labels[0, padded_prompt_len:], torch.tensor(completion_ids, dtype=torch.long)) assert prompt_texts[0] == llama_tokenizer.decode(prompt_ids, skip_special_tokens=False) assert completion_texts[0] == llama_tokenizer.decode(completion_ids, skip_special_tokens=False) @pytest.mark.slow def test_generate_on_policy_outputs_masks_prompt_smollm(smollm_tokenizer, openr1_examples): trainer = GOLDTrainer.__new__(GOLDTrainer) trainer.use_transformers_paged = False trainer.processing_class = smollm_tokenizer collator = DataCollatorForChatML(tokenizer=smollm_tokenizer) batch = collator([openr1_examples[0]]) batch = {k: v.cpu() for k, v in batch.items()} class DummyModel: def generate(self, input_ids, attention_mask, generation_config, return_dict_in_generate): assert torch.equal(input_ids, batch["prompts"]) assert torch.equal(attention_mask, batch["prompt_attention_mask"]) return SimpleNamespace(sequences=batch["input_ids"]) generation_config = SimpleNamespace(max_completion_length=None, temperature=None, top_k=None, top_p=None) pad_id = smollm_tokenizer.pad_token_id new_ids, new_mask, new_labels, prompt_texts, completion_texts = GOLDTrainer.generate_on_policy_outputs( trainer, DummyModel(), {"prompts": batch["prompts"], "prompt_attention_mask": batch["prompt_attention_mask"]}, generation_config, pad_id, ) assert torch.equal(new_ids, batch["input_ids"]) if pad_id is not None: expected_mask = (batch["input_ids"] != pad_id).long() assert torch.equal(new_mask, expected_mask) else: assert torch.all(new_mask == 1) prompt_len = int(batch["prompt_attention_mask"].sum().item()) tail_labels = new_labels[0, prompt_len:] expected_tail = batch["input_ids"][0, prompt_len:] active_mask = tail_labels != -100 assert torch.all(new_labels[0, :prompt_len] == -100) assert torch.equal(tail_labels[active_mask], expected_tail[active_mask]) assert torch.all(tail_labels[~active_mask] == -100) prompt_tokens = batch["prompts"][0, batch["prompt_attention_mask"][0].bool()] decoded_prompt = smollm_tokenizer.decode(prompt_tokens.tolist(), skip_special_tokens=False) assert prompt_texts[0] == decoded_prompt assistant_completion = openr1_examples[0]["messages"][-1]["content"].strip() assert assistant_completion in completion_texts[0] def test_generalized_jsd_loss_accepts_probability_inputs(): student_probs = torch.tensor([[[0.6, 0.3, 0.1]]]) teacher_probs = torch.tensor([[[0.5, 0.4, 0.1]]]) mixture = 0.5 * (student_probs + teacher_probs) expected = 0.5 * ( torch.sum(student_probs * (torch.log(student_probs) - torch.log(mixture))) + torch.sum(teacher_probs * (torch.log(teacher_probs) - torch.log(mixture))) ) loss = GOLDTrainer.generalized_jsd_loss( student_probs, teacher_probs, beta=0.5, reduction="batchmean", logits_are_probs=True, ) torch.testing.assert_close(loss, expected) def test_uldloss_handles_llama_student_qwen_teacher_sequence(llama_tokenizer, qwen_tokenizer): config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.6, uld_hybrid_unmatched_weight=0.4, ) loss_fn = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) prompt = "User: Summarize the difference between llamas and alpacas." completion = "Assistant: Llamas are taller while alpacas have softer wool." student_ids, student_labels = encode_prompt_completion(llama_tokenizer, prompt, completion) teacher_ids, teacher_labels = encode_prompt_completion(qwen_tokenizer, prompt, completion) pad_id_student = llama_tokenizer.pad_token_id pad_id_teacher = qwen_tokenizer.pad_token_id max_length = max(len(student_ids), len(teacher_ids)) student_ids = pad_tokens(student_ids, pad_id_student, max_length) teacher_ids = pad_tokens(teacher_ids, pad_id_teacher, max_length) student_labels = pad_labels(student_labels, max_length) teacher_labels = pad_labels(teacher_labels, max_length) student_input_ids = torch.tensor([student_ids]) teacher_input_ids = torch.tensor([teacher_ids]) student_labels = torch.tensor([student_labels]) teacher_labels = torch.tensor([teacher_labels]) student_vocab = len(llama_tokenizer) teacher_vocab = len(qwen_tokenizer) student_logits = torch.randn(1, max_length, student_vocab) teacher_logits = torch.randn(1, max_length, teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=student_labels, teacher_labels=teacher_labels, student_input_ids=student_input_ids, teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) assert loss.dim() == 0 assert loss_fn.last_matched_loss is not None assert loss_fn.last_unmatched_loss is not None def test_uldloss_handles_smollm_student_qwen_teacher_sequence(smollm_tokenizer, qwen_tokenizer): config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.5, uld_hybrid_unmatched_weight=0.5, ) loss_fn = ULDLoss(config, student_tokenizer=smollm_tokenizer, teacher_tokenizer=qwen_tokenizer) prompt = "User: Describe SmolLM3 in a sentence." completion = "Assistant: SmolLM3 is a compact yet capable language model." student_ids, student_labels = encode_prompt_completion(smollm_tokenizer, prompt, completion) teacher_ids, teacher_labels = encode_prompt_completion(qwen_tokenizer, prompt, completion) pad_id_student = smollm_tokenizer.pad_token_id pad_id_teacher = qwen_tokenizer.pad_token_id max_length = max(len(student_ids), len(teacher_ids)) student_ids = pad_tokens(student_ids, pad_id_student, max_length) teacher_ids = pad_tokens(teacher_ids, pad_id_teacher, max_length) student_labels = pad_labels(student_labels, max_length) teacher_labels = pad_labels(teacher_labels, max_length) student_input_ids = torch.tensor([student_ids]) teacher_input_ids = torch.tensor([teacher_ids]) student_labels = torch.tensor([student_labels]) teacher_labels = torch.tensor([teacher_labels]) student_vocab = len(smollm_tokenizer) teacher_vocab = len(qwen_tokenizer) student_logits = torch.randn(1, max_length, student_vocab) teacher_logits = torch.randn(1, max_length, teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=student_labels, teacher_labels=teacher_labels, student_input_ids=student_input_ids, teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) assert loss.dim() == 0 assert loss_fn.last_matched_loss is not None assert loss_fn.last_unmatched_loss is not None def test_uldloss_hybrid_config_beta_zero(llama_tokenizer, qwen_tokenizer): config = build_config( uld_use_hybrid_loss=True, uld_hybrid_matched_weight=0.0, uld_hybrid_unmatched_weight=1.0, use_extended_uld=True, uld_crossentropy_weight=0.0, uld_distillation_weight=1.0, uld_student_temperature=1.0, uld_teacher_temperature=1.0, temperature=1.0, top_p=0.95, top_k=0, lmbda=1.0, beta=0.0, ) loss_fn = ULDLoss(config, student_tokenizer=llama_tokenizer, teacher_tokenizer=qwen_tokenizer) prompt = "User: Explain how GOLD handles tokenizer mismatches." completion = "Assistant: GOLD merges aligned subwords and applies hybrid ULD loss." student_ids, student_labels = encode_prompt_completion(llama_tokenizer, prompt, completion) teacher_ids, teacher_labels = encode_prompt_completion(qwen_tokenizer, prompt, completion) pad_id_student = llama_tokenizer.pad_token_id pad_id_teacher = qwen_tokenizer.pad_token_id max_length = max(len(student_ids), len(teacher_ids)) student_ids = pad_tokens(student_ids, pad_id_student, max_length) teacher_ids = pad_tokens(teacher_ids, pad_id_teacher, max_length) student_labels = pad_labels(student_labels, max_length) teacher_labels = pad_labels(teacher_labels, max_length) student_input_ids = torch.tensor([student_ids]) teacher_input_ids = torch.tensor([teacher_ids]) student_labels = torch.tensor([student_labels]) teacher_labels = torch.tensor([teacher_labels]) student_vocab = len(llama_tokenizer) teacher_vocab = len(qwen_tokenizer) torch.manual_seed(0) student_logits = torch.randn(1, max_length, student_vocab) teacher_logits = torch.randn(1, max_length, teacher_vocab) loss = loss_fn( student_logits=student_logits, teacher_logits=teacher_logits, student_labels=student_labels, teacher_labels=teacher_labels, student_input_ids=student_input_ids, teacher_input_ids=teacher_input_ids, ) assert torch.isfinite(loss) assert loss.dim() == 0 assert loss_fn.last_matched_loss is not None assert loss_fn.last_unmatched_loss is not None expected = config.uld_hybrid_unmatched_weight * loss_fn.last_unmatched_loss torch.testing.assert_close(loss, expected, atol=1e-6, rtol=1e-5) ================================================ FILE: tests/experimental/test_grpo_with_replay_buffer_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from trl.experimental.grpo_with_replay_buffer import ( GRPOWithReplayBufferConfig, GRPOWithReplayBufferTrainer, ReplayBuffer, ) from ..testing_utils import TrlTestCase @pytest.mark.low_priority class TestReplayBuffer: def setup_method(self): self.replay_buffer = ReplayBuffer(max_size=5) def test_add(self): # Add elements to the replay buffer scores = [0.5, 0.8, 0.3, 0.9, 0.7] data = [ {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}, ] self.replay_buffer.add(scores, data) # Check if the buffer contains the correct number of elements assert len(self.replay_buffer.heap) == 5 # Check if the buffer maintains the min-heap property heap_scores = [item[0] for item in self.replay_buffer.heap] assert heap_scores[0] == min(heap_scores) assert heap_scores[0] == 0.3 def test_add_more_than_maxlen(self): # Add elements to the replay buffer scores = [0.5, 0.8, 0.3, 0.9, 0.7, 0.6, 0.4] data = [ {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}, {"id": 6}, {"id": 7}, ] self.replay_buffer.add(scores, data) # Check if the buffer contains the correct number of elements assert len(self.replay_buffer.heap) == 5 # Check if the buffer maintains the min-heap property heap_scores = [item[0] for item in self.replay_buffer.heap] assert heap_scores[0] == min(heap_scores) assert heap_scores[0] == 0.5 # 0.3 and 0.4 should be removed def test_sample(self): # Add elements to the replay buffer scores = [0.5, 0.8, 0.3, 0.9, 0.7] data = [ {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}, ] self.replay_buffer.add(scores, data) # Sample elements from the buffer sampled = self.replay_buffer.sample(num_samples=3) # Check if the sampled elements are from the buffer assert len(sampled) == 3 for item in sampled: assert item in [entry[1] for entry in self.replay_buffer.heap] @pytest.mark.low_priority class TestUpdateWithReplayBuffer: def setup_method(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") config = GRPOWithReplayBufferConfig( replay_buffer_size=5, ) self.trainer = GRPOWithReplayBufferTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=config, train_dataset=dataset, ) self.trainer.replay_buffer = ReplayBuffer(max_size=5) self.trainer.num_generations = 2 def _prepopulate_buffer(self, with_pixels=False, with_logprobs=False): scores = [0.1, 0.9] data = [ { "prompt_ids": torch.tensor([[100, 101], [102, 103]]), "prompt_mask": torch.ones(2, 2, dtype=torch.long), "completion_ids": torch.tensor([[5, 6], [7, 8]]), "completion_mask": torch.ones(2, 2, dtype=torch.long), "advantages": torch.tensor([[0.5, 0.6]]), **({"pixel_values": torch.randn(2, 3, 224, 224)} if with_pixels else {}), **({"old_per_token_logps": torch.randn(2, 2)} if with_logprobs else {}), }, { "prompt_ids": torch.tensor([[104, 105], [106, 107]]), "prompt_mask": torch.ones(2, 2, dtype=torch.long), "completion_ids": torch.tensor([[13, 14], [15, 16]]), "completion_mask": torch.ones(2, 2, dtype=torch.long), "advantages": torch.tensor([[0.8, 0.85]]), **({"pixel_values": torch.randn(2, 3, 224, 224)} if with_pixels else {}), **({"old_per_token_logps": torch.randn(2, 2)} if with_logprobs else {}), }, ] self.trainer.replay_buffer.add(scores, data) def _make_inputs(self, group_advantages, with_pixels=False, with_logprobs=False): inputs = { "group_advantages": group_advantages, "prompt_ids": torch.tensor([[1, 2], [3, 4], [5, 6], [7, 8]]), "prompt_mask": torch.ones(4, 2, dtype=torch.long), "completion_ids": torch.tensor([[9, 10], [11, 12], [13, 14], [15, 16]]), "completion_mask": torch.ones(4, 2, dtype=torch.long), "forward_kwargs": {"pixel_values": torch.randn(4, 3, 224, 224)} if with_pixels else {}, "old_per_token_logps": torch.randn(4, 2) if with_logprobs else None, } inputs["group_std_rewards"] = group_advantages.std(dim=1).expand_as(group_advantages) return inputs def test_update_with_replay_buffer_no_variance(self): self._prepopulate_buffer(with_pixels=True, with_logprobs=True) group_advantages = torch.tensor([[0.5, 0.5], [0.8, 0.8]]) # no variance inputs = self._make_inputs(group_advantages, with_pixels=True, with_logprobs=True) original_prompt_ids = inputs["prompt_ids"].clone() outputs = self.trainer.update_with_replay_buffer(**inputs, num_items_in_batch=4) assert outputs is not None assert "pixel_values" in outputs assert "old_per_token_logps" in outputs assert len(self.trainer.replay_buffer.heap) == 2 for pid in outputs["prompt_ids"]: assert pid.tolist() not in original_prompt_ids.tolist() def test_update_with_replay_buffer_with_variance(self): self._prepopulate_buffer() group_advantages = torch.tensor([[0.6, 0.4], [0.7, 1.2]]) # has variance inputs = self._make_inputs(group_advantages) sampled = self.trainer.update_with_replay_buffer(**inputs, num_items_in_batch=4) assert len(self.trainer.replay_buffer.heap) == 4 # grew assert sampled is None def test_update_with_mixed_variance(self): self._prepopulate_buffer() group_advantages = torch.tensor([[0.6, 0.6], [0.3, 0.45]]) # one no-variance, one variance inputs = self._make_inputs(group_advantages) original_prompt_ids = inputs["prompt_ids"].clone().view(-1, self.trainer.num_generations, 2).tolist() outputs = self.trainer.update_with_replay_buffer(**inputs, num_items_in_batch=4) assert len(self.trainer.replay_buffer.heap) == 3 # grew by 1 output_prompt_ids = outputs["prompt_ids"].view(-1, self.trainer.num_generations, 2).tolist() buffer_ids = [item[1]["prompt_ids"].tolist() for item in self.trainer.replay_buffer.heap] found_from_buffer = any(pid in buffer_ids for pid in output_prompt_ids) found_from_original = any(pid in original_prompt_ids for pid in output_prompt_ids) assert found_from_buffer assert found_from_original assert [[1, 2], [3, 4]] not in output_prompt_ids # excluded no-variance group def test_update_with_inputs_different_seq_len(self): """ Test with inputs where the sequence lengths are different from the prepopulated buffer. """ self._prepopulate_buffer() pad_token_id = self.trainer.processing_class.pad_token_id group_advantages = torch.tensor([[0.6, 0.6], [0.3, 0.45]]) # one no-variance, one variance inputs = { "group_advantages": group_advantages, "prompt_ids": torch.tensor( [ [1, 2, pad_token_id], [1, 2, pad_token_id], [3, 4, 5], [3, 4, 5], ] ), "prompt_mask": torch.tensor([[1, 1, 0], [1, 1, 0], [1, 1, 1], [1, 1, 1]], dtype=torch.long), "completion_ids": torch.tensor( [ [1009, 1010, pad_token_id], [1011, 1012, 1013], [1013, 1014, pad_token_id], [1015, 1016, 1017], ] ), "completion_mask": torch.tensor([[1, 1, 0], [1, 1, 1], [1, 1, 0], [1, 1, 1]], dtype=torch.long), "forward_kwargs": {}, } inputs["group_std_rewards"] = group_advantages.std(dim=1).expand_as(group_advantages) outputs_after_sampling = self.trainer.update_with_replay_buffer(**inputs, num_items_in_batch=4) # Seq length of current batch should be preserved assert outputs_after_sampling["prompt_ids"].shape[-1] == 3 assert len(self.trainer.replay_buffer.heap) == 3 output_prompt_ids = outputs_after_sampling["prompt_ids"].view(-1, self.trainer.num_generations, 3).tolist() buffered_prompt_completion_ids = [ (item[1]["prompt_ids"].tolist(), item[1]["completion_ids"].tolist()) for item in self.trainer.replay_buffer.heap ] buffered_prompt_ids, buffered_completion_ids = zip(*buffered_prompt_completion_ids, strict=True) # Check for new entry with seq len 3 in buffer assert [[3, 4, 5], [3, 4, 5]] in buffered_prompt_ids # excluded no-variance group assert [ [1013, 1014, pad_token_id], [1015, 1016, 1017], ] in buffered_completion_ids # excluded no-variance group # Check that sampled outputs contain one group with prompt_ids starting with a pad token assert [ [pad_token_id, 101, 102], [pad_token_id, 102, 103], ] in output_prompt_ids or [ [pad_token_id, 104, 105], [pad_token_id, 106, 107], ] in output_prompt_ids @pytest.mark.low_priority @pytest.mark.parametrize("scale_rewards", ["batch", "group"]) class TestGRPOWithReplayBufferTrainer(TrlTestCase): def test_training_with_replay_buffer(self, scale_rewards): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Guarantee that some rewards have 0 std def custom_reward_func(completions, **kwargs): if torch.rand(1).item() < 0.25: return [0] * len(completions) # simulate some None rewards else: return torch.rand(len(completions)).tolist() training_args = GRPOWithReplayBufferConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=4, # reduce the batch size to reduce memory usage num_generations=4, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage replay_buffer_size=8, report_to="none", scale_rewards=scale_rewards, ) trainer = GRPOWithReplayBufferTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[custom_reward_func], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." ================================================ FILE: tests/experimental/test_gspo_token_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch from datasets import load_dataset from transformers.utils import is_peft_available from trl import GRPOConfig from trl.experimental.gspo_token import GRPOTrainer as GSPOTokenTrainer from ..testing_utils import TrlTestCase if is_peft_available(): pass class TestGSPOTokenTrainer(TrlTestCase): def test_training(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_iterations=2, # the importance sampling weights won't be 0 in this case importance_sampling_level="sequence_token", report_to="none", ) trainer = GSPOTokenTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." ================================================ FILE: tests/experimental/test_judges.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import random import sys import time import pytest import transformers from packaging.version import Version from trl.experimental.judges import AllTrueJudge, BaseBinaryJudge, HfPairwiseJudge, PairRMJudge from ..testing_utils import TrlTestCase, require_llm_blender class RandomBinaryJudge(BaseBinaryJudge): """ Random binary judge, for testing purposes. """ def judge(self, prompts, completions, gold_completions=None, shuffle_order=True): return [random.choice([0, 1, -1]) for _ in range(len(prompts))] class TestJudges(TrlTestCase): def _get_prompts_and_pairwise_completions(self): prompts = ["The capital of France is", "The biggest planet in the solar system is"] completions = [["Paris", "Marseille"], ["Saturn", "Jupiter"]] return prompts, completions def _get_prompts_and_single_completions(self): prompts = ["What's the capital of France?", "What's the color of the sky?"] completions = ["Marseille", "blue"] return prompts, completions def test_all_true_judge(self): judge = AllTrueJudge(judges=[RandomBinaryJudge(), RandomBinaryJudge()]) prompts, completions = self._get_prompts_and_single_completions() judgements = judge.judge(prompts=prompts, completions=completions) assert len(judgements) == 2 assert all(judgement in {0, 1, -1} for judgement in judgements) @pytest.mark.skip(reason="This test needs to be run manually since it requires a valid Hugging Face API key.") def test_hugging_face_judge(self): judge = HfPairwiseJudge() prompts, completions = self._get_prompts_and_pairwise_completions() ranks = judge.judge(prompts=prompts, completions=completions) assert len(ranks) == 2 assert all(isinstance(rank, int) for rank in ranks) assert ranks == [0, 1] def load_pair_rm_judge(self): # When using concurrent tests, PairRM may fail to load the model while another job is still downloading. # This is a workaround to retry loading the model a few times. for _ in range(5): try: return PairRMJudge() except ValueError: time.sleep(5) raise ValueError("Failed to load PairRMJudge") @require_llm_blender @pytest.mark.skipif( sys.version_info[:3] == (3, 13, 8), reason="Python 3.13.8 has a bug in inspect.BlockFinder (cpython GH-139783)" ) @pytest.mark.xfail( Version(transformers.__version__) >= Version("5.0.0"), reason="Known incompatibility between llm-blender and transformers >= 5.0.0 (GH-4918)", strict=True, ) def test_pair_rm_judge(self): judge = self.load_pair_rm_judge() prompts, completions = self._get_prompts_and_pairwise_completions() ranks = judge.judge(prompts=prompts, completions=completions) assert len(ranks) == 2 assert all(isinstance(rank, int) for rank in ranks) assert ranks == [0, 1] @require_llm_blender @pytest.mark.skipif( sys.version_info[:3] == (3, 13, 8), reason="Python 3.13.8 has a bug in inspect.BlockFinder (cpython GH-139783)" ) @pytest.mark.xfail( Version(transformers.__version__) >= Version("5.0.0"), reason="Known incompatibility between llm-blender and transformers >= 5.0.0 (GH-4918)", strict=True, ) def test_pair_rm_judge_return_scores(self): judge = self.load_pair_rm_judge() prompts, completions = self._get_prompts_and_pairwise_completions() probs = judge.judge(prompts=prompts, completions=completions, return_scores=True) assert len(probs) == 2 assert all(isinstance(prob, float) for prob in probs) assert all(0 <= prob <= 1 for prob in probs) ================================================ FILE: tests/experimental/test_kto_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import multiprocess import pytest import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl.experimental.kto import KTOConfig, KTOTrainer from trl.experimental.kto.kto_trainer import _get_kl_dataset, _process_tokens, _tokenize from ..testing_utils import TrlTestCase, require_liger_kernel, require_no_wandb, require_peft class TestKTOTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.ref_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token @pytest.mark.parametrize( "config_name, loss_type, pre_compute, eval_dataset", [ ("standard_preference", "kto", True, True), ("standard_unpaired_preference", "kto", False, True), ("conversational_implicit_prompt_preference", "apo_zero_unpaired", True, True), ("standard_unpaired_preference", "apo_zero_unpaired", False, True), ], ) def test_kto_trainer(self, config_name, loss_type, pre_compute, eval_dataset): training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps" if eval_dataset else "no", beta=0.1, precompute_ref_log_probs=pre_compute, loss_type=loss_type, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = KTOTrainer( model=self.model, ref_model=self.ref_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"] if eval_dataset else None, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) def test_kto_trainer_with_ref_model_is_model(self): training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") with pytest.raises(ValueError): KTOTrainer( model=self.model, ref_model=self.model, # ref_model can't be the same as model args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], ) def test_tokenize_and_process_tokens(self): # Pytest/CI often starts background threads before tests run. Under Python 3.12+, # using "fork" in a multi-threaded process emits a DeprecationWarning and may deadlock. # Force "spawn" to keep this multiprocessing test safe while still exercising `num_proc=2`. multiprocess.set_start_method("spawn", force=True) training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", beta=0.1, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") trainer = KTOTrainer( model=self.model, ref_model=self.ref_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) train_dataset = dummy_dataset["train"] tokenized_dataset = train_dataset.map( _tokenize, fn_kwargs={"tokenizer": trainer.processing_class}, batched=True, batch_size=2, ) assert tokenized_dataset["prompt"][:] == train_dataset["prompt"][:] assert tokenized_dataset["completion"][:] == train_dataset["completion"][:] assert tokenized_dataset["label"][:] == train_dataset["label"][:] assert tokenized_dataset["prompt_input_ids"][0] == [46518, 374, 2664, 1091] assert tokenized_dataset["prompt_attention_mask"][0] == [1, 1, 1, 1] assert tokenized_dataset["answer_input_ids"][0] == [27261, 13] assert tokenized_dataset["answer_attention_mask"][0] == [1, 1] # Test corruption of (prompt, completion) pairs for KL dataset for batch_size in [2, 3]: tokenized_kl_dataset = tokenized_dataset.map(_get_kl_dataset, batched=True, batch_size=batch_size) # Verify that the "answer_input_ids" have been modified, meaning the new "answer_input_ids" differ # from the original ones. However, when the length of the dataset modulo batch_size equals 1, # the last batch remains unaltered. This is a rare scenario that does not impact the training # process, so we exclude it from testing by iterating only up to len - 1. for i in range(len(tokenized_kl_dataset["answer_input_ids"]) - 1): assert tokenized_dataset["prompt_input_ids"][i] == tokenized_kl_dataset["prompt_input_ids"][i] assert ( tokenized_dataset["prompt_attention_mask"][i] == tokenized_kl_dataset["prompt_attention_mask"][i] ) assert tokenized_dataset["answer_input_ids"][i] != tokenized_kl_dataset["answer_input_ids"][i] fn_kwargs = { "prefix": "", "tokenizer": trainer.processing_class, "max_length": trainer.max_length, } processed_dataset = tokenized_dataset.map(_process_tokens, fn_kwargs=fn_kwargs, num_proc=2) assert processed_dataset["prompt"][:] == train_dataset["prompt"][:] assert processed_dataset["completion"][:] == train_dataset["completion"][:] assert processed_dataset["label"][:] == train_dataset["label"][:] assert processed_dataset["prompt_input_ids"][0] == [46518, 374, 2664, 1091] assert processed_dataset["prompt_attention_mask"][0] == [1, 1, 1, 1] assert processed_dataset["completion_input_ids"][0] == [46518, 374, 2664, 1091, 27261, 13, 151645] assert processed_dataset["completion_attention_mask"][0] == [1, 1, 1, 1, 1, 1, 1] assert processed_dataset["completion_labels"][0] == [-100, -100, -100, -100, 27261, 13, 151645] def test_kto_trainer_without_providing_ref_model(self): training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=4, learning_rate=9e-1, eval_strategy="steps", beta=0.1, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") trainer = KTOTrainer( model=self.model, ref_model=None, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) @require_peft def test_kto_trainer_without_providing_ref_model_with_lora(self): from peft import LoraConfig lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=4, learning_rate=9e-1, eval_strategy="steps", beta=0.1, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") trainer = KTOTrainer( model=self.model, ref_model=None, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): if "lora" in n: new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) @require_no_wandb def test_kto_trainer_generate_during_eval_no_wandb(self): training_args = KTOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", beta=0.1, generate_during_eval=True, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") with pytest.raises( ValueError, match="`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve.", ): KTOTrainer( model=self.model, ref_model=None, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) @require_liger_kernel def test_kto_trainer_with_liger(self): """Test KTO trainer with Liger kernel enabled.""" training_args = KTOConfig( output_dir=self.tmp_dir, report_to="none", use_liger_kernel=True, # Enable Liger kernel ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") trainer = KTOTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # check the params have changed - ignore 0 biases if param.sum() != 0: assert not torch.equal(param, new_param) def test_compute_metrics(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") ref_model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") tokenizer.pad_token = tokenizer.eos_token dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_unpaired_preference") def dummy_compute_metrics(*args, **kwargs): return {"test": 0.0} training_args = KTOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, per_device_train_batch_size=2, do_eval=True, eval_strategy="steps", eval_steps=1, per_device_eval_batch_size=2, report_to="none", ) trainer = KTOTrainer( model=model, ref_model=ref_model, args=training_args, processing_class=tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], compute_metrics=dummy_compute_metrics, ) trainer.train() assert trainer.state.log_history[-2]["eval_test"] == 0.0 ================================================ FILE: tests/experimental/test_merge_model_callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from transformers.trainer_utils import get_last_checkpoint from trl import DPOConfig, DPOTrainer from trl.experimental.merge_model_callback import MergeConfig, MergeModelCallback from ..testing_utils import TrlTestCase, require_mergekit @require_mergekit class TestMergeModelCallback(TrlTestCase): def setup_method(self): self.model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32" ) self.tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") def test_callback(self): training_args = DPOConfig( output_dir=self.tmp_dir, num_train_epochs=1, report_to="none", save_strategy="steps", save_steps=1, ) config = MergeConfig() merge_callback = MergeModelCallback(config) trainer = DPOTrainer( model=self.model, args=training_args, train_dataset=self.dataset, processing_class=self.tokenizer, callbacks=[merge_callback], ) trainer.train() last_checkpoint = get_last_checkpoint(self.tmp_dir) merged_path = os.path.join(last_checkpoint, "merged") assert os.path.isdir(merged_path), "Merged folder does not exist in the last checkpoint." def test_every_checkpoint(self): training_args = DPOConfig( output_dir=self.tmp_dir, num_train_epochs=1, report_to="none", save_strategy="steps", save_steps=1, ) config = MergeConfig() merge_callback = MergeModelCallback(config, merge_at_every_checkpoint=True) trainer = DPOTrainer( model=self.model, args=training_args, train_dataset=self.dataset, processing_class=self.tokenizer, callbacks=[merge_callback], ) trainer.train() checkpoints = sorted( [os.path.join(self.tmp_dir, cp) for cp in os.listdir(self.tmp_dir) if cp.startswith("checkpoint-")] ) for checkpoint in checkpoints: merged_path = os.path.join(checkpoint, "merged") assert os.path.isdir(merged_path), f"Merged folder does not exist in checkpoint {checkpoint}." ================================================ FILE: tests/experimental/test_minillm_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from trl.experimental.minillm import MiniLLMConfig, MiniLLMTrainer from ..testing_utils import TrlTestCase @pytest.mark.low_priority class TestMiniLLMTrainer(TrlTestCase): def test_train(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Initialize the trainer training_args = MiniLLMConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=32, # reduce the completion length to reduce memory usage report_to="none", ) trainer = MiniLLMTrainer( model="trl-internal-testing/small-Qwen3ForCausalLM", teacher_model="trl-internal-testing/tiny-Qwen3ForCausalLM", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" ================================================ FILE: tests/experimental/test_modeling_value_head.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch from trl.experimental.ppo import AutoModelForCausalLMWithValueHead from trl.experimental.utils import create_reference_model from ..testing_utils import TrlTestCase class TestReferenceModel(TrlTestCase): def setup_method(self): self.model = AutoModelForCausalLMWithValueHead.from_pretrained("trl-internal-testing/tiny-GPT2LMHeadModel") self.test_input = torch.tensor([[0, 1, 2, 3]]) self.optimizer = torch.optim.AdamW(self.model.parameters(), lr=1) self.layer_format = "pretrained_model.transformer.h.{layer}.attn.c_attn.weight" def test_independent_reference(self): layer_0 = self.layer_format.format(layer=0) layer_1 = self.layer_format.format(layer=1) ref_model = create_reference_model(self.model) first_layer_before = self.model.get_parameter(layer_0).data.clone() last_layer_before = self.model.get_parameter(layer_1).data.clone() # the model only has 2 layers first_ref_layer_before = ref_model.get_parameter(layer_0).data.clone() last_ref_layer_before = ref_model.get_parameter(layer_1).data.clone() output = self.model(input_ids=self.test_input, labels=self.test_input) output[1].backward() self.optimizer.step() first_layer_after = self.model.get_parameter(layer_0).data.clone() last_layer_after = self.model.get_parameter(layer_1).data.clone() first_ref_layer_after = ref_model.get_parameter(layer_0).data.clone() last_ref_layer_after = ref_model.get_parameter(layer_1).data.clone() # before optimization ref and model are identical assert (first_layer_before == first_ref_layer_before).all() assert (last_layer_before == last_ref_layer_before).all() # ref model stays identical after optimization assert (first_ref_layer_before == first_ref_layer_after).all() assert (last_ref_layer_before == last_ref_layer_after).all() # optimized model changes assert not (first_layer_before == first_layer_after).all() assert not (last_layer_before == last_layer_after).all() def test_shared_layers(self): layer_0 = self.layer_format.format(layer=0) layer_1 = self.layer_format.format(layer=1) ref_model = create_reference_model(self.model, num_shared_layers=1) first_layer_before = self.model.get_parameter(layer_0).data.clone() second_layer_before = self.model.get_parameter(layer_1).data.clone() first_ref_layer_before = ref_model.get_parameter(layer_0).data.clone() second_ref_layer_before = ref_model.get_parameter(layer_1).data.clone() output = self.model(input_ids=self.test_input, labels=self.test_input) output[1].backward() self.optimizer.step() first_layer_after = self.model.get_parameter(layer_0).data.clone() second_layer_after = self.model.get_parameter(layer_1).data.clone() first_ref_layer_after = ref_model.get_parameter(layer_0).data.clone() second_ref_layer_after = ref_model.get_parameter(layer_1).data.clone() # before optimization ref and model are identical assert (first_layer_before == first_ref_layer_before).all() assert (second_layer_before == second_ref_layer_before).all() # ref model stays identical after optimization assert (first_ref_layer_before == first_ref_layer_after).all() assert (second_ref_layer_before == second_ref_layer_after).all() # first layer of optimized model stays the same assert (first_layer_before == first_layer_after).all() # other layers in optimized model change assert not (second_layer_before == second_layer_after).all() ================================================ FILE: tests/experimental/test_nash_md_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, GenerationConfig from transformers.utils import is_peft_available from trl.experimental.nash_md import NashMDConfig, NashMDTrainer from trl.experimental.nash_md.nash_md_trainer import GeometricMixtureWrapper from trl.experimental.utils import create_reference_model from ..testing_utils import TrlTestCase, require_llm_blender, require_peft from .testing_utils import RandomPairwiseJudge if is_peft_available(): from peft import LoraConfig, get_peft_model class TestGeometricMixtureWrapper(TrlTestCase): def setup_method(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.device = "cuda" if torch.cuda.is_available() else "cpu" self.model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32").to(self.device) self.ref_model = create_reference_model(self.model).to(self.device) self.generation_config = GenerationConfig.from_pretrained(model_id) self.mixture_coef = 0.5 self.wrapper = GeometricMixtureWrapper( self.model, self.ref_model, self.generation_config, mixture_coef=self.mixture_coef ) def test_forward(self): input_ids = torch.tensor([[1, 2, 3, 4, 5]], device=self.device) attention_mask = torch.ones_like(input_ids) output = self.wrapper(input_ids=input_ids, attention_mask=attention_mask) assert output is not None assert hasattr(output, "logits") assert output.logits.shape == (1, 5, self.model.config.vocab_size) def test_mixture_coefficient(self): input_ids = torch.tensor([[1, 2, 3, 4, 5]], device=self.device) attention_mask = torch.ones_like(input_ids) with torch.no_grad(): model_output = self.model(input_ids=input_ids, attention_mask=attention_mask) ref_model_output = self.ref_model(input_ids=input_ids, attention_mask=attention_mask) wrapper_output = self.wrapper(input_ids=input_ids, attention_mask=attention_mask) expected_logits = torch.nn.functional.log_softmax( self.mixture_coef * ref_model_output.logits + (1 - self.mixture_coef) * model_output.logits, dim=-1 ) torch.testing.assert_close(wrapper_output.logits, expected_logits) def test_prepare_inputs_for_generation(self): input_ids = torch.tensor([[1, 2, 3, 4, 5]], device=self.device) attention_mask = torch.ones_like(input_ids) inputs = self.wrapper.prepare_inputs_for_generation(input_ids, attention_mask=attention_mask, use_cache=True) assert "input_ids" in inputs assert "attention_mask" in inputs assert not inputs.get("use_cache", False) class TestNashMDTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.ref_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.reward_model = AutoModelForSequenceClassification.from_pretrained(self.model_id, num_labels=1) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_nash_md_trainer_training(self, config_name): training_args = NashMDConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = NashMDTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_with_peft(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = NashMDConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = NashMDTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_with_peft_and_ref_model(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = NashMDConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = NashMDTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_pre_pefted_model_implicit_ref_with_reward_model(self): lora_config = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.1, bias="none", task_type="CAUSAL_LM") # self.model from setUp is a base AutoModelForCausalLM peft_model_instance = get_peft_model(self.model, lora_config) training_args = NashMDConfig( output_dir=self.tmp_dir, per_device_train_batch_size=1, # Keep small for quick test max_steps=2, # Few steps learning_rate=5.0e-7, eval_strategy="no", report_to="none", remove_unused_columns=False, # Important for the dummy dataset ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only")["train"] trainer = NashMDTrainer( model=peft_model_instance, # Pass the already PEFT model ref_model=None, # Implicit reference from peft_model_instance's base reward_funcs=self.reward_model, # To trigger GeometricMixtureWrapper path args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset, # peft_config is not passed, as model is already PEFT ) trainer.train() assert "train_loss" in trainer.state.log_history[-1] @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) @require_llm_blender def test_nash_md_trainer_judge_training(self, config_name): training_args = NashMDConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) judge = RandomPairwiseJudge() trainer = NashMDTrainer( model=self.model, ref_model=self.ref_model, judge=judge, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] ================================================ FILE: tests/experimental/test_online_dpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import transformers from datasets import Dataset, features, load_dataset from packaging.version import Version from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer from transformers.utils import is_peft_available, is_vision_available from trl.experimental.online_dpo import OnlineDPOConfig, OnlineDPOTrainer from ..testing_utils import ( TrlTestCase, require_llm_blender, require_peft, require_torch_accelerator, require_vision, require_vllm, ) from .testing_utils import RandomPairwiseJudge if is_peft_available(): from peft import LoraConfig if is_vision_available(): import numpy as np from PIL import Image from transformers import AutoModelForImageTextToText, AutoProcessor class TestOnlineDPOTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.ref_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token self.reward_model_id = "trl-internal-testing/tiny-LlamaForCausalLM-3.2" self.reward_model = AutoModelForSequenceClassification.from_pretrained(self.reward_model_id, num_labels=1) self.reward_tokenizer = AutoTokenizer.from_pretrained(self.reward_model_id) self.reward_tokenizer.pad_token = self.reward_tokenizer.eos_token @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_training(self, config_name): training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = OnlineDPOTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] def test_training_model_str(self): training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] def test_training_with_ref_model(self): training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] def test_ref_model_is_model(self): training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") with pytest.raises(ValueError): OnlineDPOTrainer( model=self.model, ref_model=self.model, # ref_model can't be the same as model reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) @require_peft def test_training_with_peft(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_with_peft_and_ref_model(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) @require_llm_blender def test_training_with_judge(self, config_name): training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = OnlineDPOTrainer( model=self.model, judge=RandomPairwiseJudge(), args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) @require_torch_accelerator @require_vllm @pytest.mark.slow def test_training_with_vllm_server(self, config_name): def cleanup_vllm_communicator(trainer): """Clean up vLLM communicator to avoid conflicts between test runs""" try: if hasattr(trainer, "vllm_client") and trainer.vllm_client is not None: trainer.vllm_client.close_communicator() except Exception: pass # Continue if cleanup fails model_id = "trl-internal-testing/small-Qwen2ForCausalLM-2.5" # We need a bigger model model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.pad_token = tokenizer.eos_token training_args = OnlineDPOConfig( output_dir=self.tmp_dir, use_vllm=True, vllm_mode="server", vllm_gpu_memory_utilization=0.2, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = OnlineDPOTrainer( model=model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], processing_class=tokenizer, reward_processing_classes=self.reward_tokenizer, ) # Ensure cleanup of vLLM communicator after the test try: trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] finally: cleanup_vllm_communicator(trainer) @require_vllm def test_training_with_vllm_colocate(self): """Test vLLM colocate mode with our refactored implementation""" model_id = "trl-internal-testing/small-Qwen2ForCausalLM-2.5" # We need a bigger model model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_id) tokenizer.pad_token = tokenizer.eos_token training_args = OnlineDPOConfig( output_dir=self.tmp_dir, use_vllm=True, vllm_mode="colocate", vllm_gpu_memory_utilization=0.2, per_device_train_batch_size=1, max_steps=2, report_to="none", # Test generation parameters temperature=0.9, top_p=0.95, top_k=50, repetition_penalty=1.1, max_new_tokens=32, ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model=model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], processing_class=tokenizer, reward_processing_classes=self.reward_tokenizer, ) # Verify vLLM setup assert trainer.use_vllm assert trainer.vllm_mode == "colocate" assert trainer.llm is not None # self.assertIsNone(trainer.vllm_client) # self.assertEqual(trainer.vllm_gpu_memory_utilization, 0.2) # Verify generation parameters assert trainer.temperature == 0.9 assert trainer.top_p == 0.95 assert trainer.top_k == 50 assert trainer.repetition_penalty == 1.1 # Verify generation config assert trainer.generation_config is not None assert trainer.generation_config.temperature == 0.9 assert trainer.generation_config.top_p == 0.95 assert trainer.generation_config.top_k == 50 assert trainer.generation_config.repetition_penalty == 1.1 assert trainer.generation_config.max_tokens == 32 trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] def test_vllm_config_validation(self): """Test vLLM configuration validation""" # Test valid vllm_mode values config = OnlineDPOConfig(use_vllm=True, vllm_mode="server") assert config.vllm_mode == "server" config = OnlineDPOConfig(use_vllm=True, vllm_mode="colocate") assert config.vllm_mode == "colocate" # Test default values config = OnlineDPOConfig() assert config.vllm_mode == "colocate" assert config.vllm_server_base_url is None assert config.vllm_server_host == "0.0.0.0" assert config.vllm_server_port == 8000 assert config.vllm_server_timeout == 240.0 assert config.vllm_gpu_memory_utilization == 0.55 # Test generation parameters assert config.top_p == 1.0 assert config.top_k == 0 assert config.min_p is None assert config.repetition_penalty == 1.0 assert not config.use_transformers_paged assert config.cache_implementation is None assert config.generation_kwargs is None def test_generation_config_setup(self): """Test that generation configuration is properly set up for both vLLM and transformers""" training_args = OnlineDPOConfig( output_dir=self.tmp_dir, use_vllm=False, temperature=0.8, top_p=0.9, top_k=40, repetition_penalty=1.2, max_new_tokens=64, generation_kwargs={"do_sample": False}, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = OnlineDPOTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) # Verify transformers generation config assert not trainer.use_vllm # When not using vLLM, these attributes should not be set assert not (hasattr(trainer, "llm") and trainer.llm is not None) assert not (hasattr(trainer, "vllm_client") and trainer.vllm_client is not None) assert trainer.generation_config is not None assert trainer.generation_config.temperature == 0.8 assert trainer.generation_config.top_p == 0.9 assert trainer.generation_config.top_k == 40 assert trainer.generation_config.repetition_penalty == 1.2 assert trainer.generation_config.max_new_tokens == 64 assert not trainer.generation_config.do_sample # From generation_kwargs @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) @require_torch_accelerator def test_training_with_transformers_paged(self, config_name): if Version(transformers.__version__) < Version("4.57.0"): pytest.xfail("Bug in transformers solved in GH#40692, released in 4.57.0.") training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", use_transformers_paged=True, ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = OnlineDPOTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, reward_processing_classes=self.reward_tokenizer, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_training_with_reward_funcs(self, config_name): def simple_reward_func(prompts, completions, completion_ids, **kwargs): return [0.5 for _ in prompts] training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", reward_weights=[0.7, 0.3], report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = OnlineDPOTrainer( model=self.model, reward_funcs=[simple_reward_func, simple_reward_func], args=training_args, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], processing_class=self.tokenizer, ) trainer.train() assert "train_loss" in trainer.state.log_history[-1] assert len(trainer.reward_funcs) == 2 assert trainer.reward_weights is not None assert round(abs(trainer.reward_weights[0].item() - 0.7), 5) == 0 assert round(abs(trainer.reward_weights[1].item() - 0.3), 5) == 0 @require_vision class TestOnlineDPOVisionTrainer(TrlTestCase): @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Idefics2ForConditionalGeneration", "trl-internal-testing/tiny-LlavaForConditionalGeneration", ], ) def test_online_dpo_vlm_trainer(self, model_id): dataset_dict = { "prompt": [ [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "Describe the image."}]}], [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What do you see?"}]}], ], "images": [ [Image.fromarray(np.random.randint(0, 255, (64, 64, 3), dtype=np.uint8))], [Image.fromarray(np.random.randint(0, 255, (64, 64, 3), dtype=np.uint8))], ], } dataset = Dataset.from_dict(dataset_dict) dataset = dataset.cast_column("images", features.Sequence(features.Image())) model = AutoModelForImageTextToText.from_pretrained(model_id, dtype="float32") reward_model = AutoModelForSequenceClassification.from_pretrained( "trl-internal-testing/tiny-LlamaForCausalLM-3.2", num_labels=1 ) processor = AutoProcessor.from_pretrained(model_id) reward_tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-LlamaForCausalLM-3.2") reward_tokenizer.pad_token = reward_tokenizer.eos_token training_args = OnlineDPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=1, max_steps=2, learning_rate=0.01, report_to="none", ) trainer = OnlineDPOTrainer( model=model, reward_funcs=reward_model, args=training_args, processing_class=processor, train_dataset=dataset, eval_dataset=dataset, reward_processing_classes=reward_tokenizer, ) trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None ================================================ FILE: tests/experimental/test_orpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSeq2SeqLM, AutoTokenizer from trl.experimental.orpo import ORPOConfig, ORPOTrainer from ..testing_utils import TrlTestCase, require_peft class TestORPOTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token # get t5 as seq2seq example: model_id = "trl-internal-testing/tiny-T5ForConditionalGeneration" self.t5_model = AutoModelForSeq2SeqLM.from_pretrained(model_id, dtype="float32") self.t5_tokenizer = AutoTokenizer.from_pretrained(model_id) @pytest.mark.parametrize( "name, config_name", [ ("qwen", "standard_preference"), ("t5", "standard_implicit_prompt_preference"), ("qwen", "conversational_preference"), ], ) def test_orpo_trainer(self, name, config_name): training_args = ORPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", beta=0.1, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) if name == "qwen": model = self.model tokenizer = self.tokenizer elif name == "t5": model = self.t5_model tokenizer = self.t5_tokenizer training_args.is_encoder_decoder = True trainer = ORPOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) @pytest.mark.parametrize( "config_name", [ "standard_preference", "standard_implicit_prompt_preference", "conversational_preference", "conversational_implicit_prompt_preference", ], ) @require_peft def test_orpo_trainer_with_lora(self, config_name): from peft import LoraConfig lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) training_args = ORPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=4, learning_rate=9e-1, eval_strategy="steps", beta=0.1, report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = ORPOTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): if "lora" in n: new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.equal(param, new_param) def test_compute_metrics(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") tokenizer.pad_token = tokenizer.eos_token dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_preference") def dummy_compute_metrics(*args, **kwargs): return {"test": 0.0} training_args = ORPOConfig( output_dir=self.tmp_dir, remove_unused_columns=False, per_device_train_batch_size=2, do_eval=True, eval_strategy="steps", eval_steps=1, per_device_eval_batch_size=2, report_to="none", ) trainer = ORPOTrainer( model=model, args=training_args, processing_class=tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], compute_metrics=dummy_compute_metrics, ) trainer.train() assert trainer.state.log_history[-2]["eval_test"] == 0.0 ================================================ FILE: tests/experimental/test_ppo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gc import os import pytest import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoModelForSeq2SeqLM, AutoModelForSequenceClassification, AutoTokenizer, GenerationConfig, ) from transformers.utils import is_peft_available from trl.experimental.ppo import ( AutoModelForCausalLMWithValueHead, AutoModelForSeq2SeqLMWithValueHead, PPOConfig, PPOTrainer, ) from trl.experimental.ppo.ppo_trainer import batch_generation, masked_mean, masked_var, masked_whiten from ..testing_utils import ( TrlTestCase, require_bitsandbytes, require_peft, require_torch_gpu_if_bnb_not_multi_backend_enabled, ) if is_peft_available(): from peft import LoraConfig, get_peft_model ALL_CAUSAL_LM_MODELS = [ "trl-internal-testing/tiny-BloomForCausalLM", "trl-internal-testing/tiny-CohereForCausalLM", # "trl-internal-testing/tiny-FalconMambaForCausalLM", # FalconMambaForCausalLM modeling seems to be broken for now "trl-internal-testing/tiny-Gemma2ForCausalLM", "trl-internal-testing/tiny-GemmaForCausalLM", "trl-internal-testing/tiny-GPT2LMHeadModel", "trl-internal-testing/tiny-GPTNeoXForCausalLM", "trl-internal-testing/tiny-LlamaForCausalLM-3.1", "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-LlamaForCausalLM-3", "trl-internal-testing/tiny-MistralForCausalLM-0.1", "trl-internal-testing/tiny-MistralForCausalLM-0.2", "trl-internal-testing/tiny-OPTForCausalLM", "trl-internal-testing/tiny-Phi3ForCausalLM", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", ] ALL_SEQ2SEQ_MODELS = [ "trl-internal-testing/tiny-T5ForConditionalGeneration", "trl-internal-testing/tiny-BartModel", ] class TestBatchGeneration(TrlTestCase): def setup_method(self): # Initialize the tokenizer self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.device = "cuda" if torch.cuda.is_available() else "cpu" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32").to(self.device) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.generation_config = GenerationConfig( max_new_tokens=128, temperature=0.5, do_sample=True, top_k=0, pad_token_id=self.tokenizer.pad_token_id, ) # Example input dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") self.examples = dataset["messages"] self.mini_batch_size = 3 def test_mini_batch_generation(self): batch = [ self.tokenizer.apply_chat_template(example[:-1], add_generation_prompt=True, tokenize=False) for example in self.examples ] queries = self.tokenizer(batch, padding=True, return_tensors="pt")["input_ids"].to(self.device) bs, context_length = queries.shape query_responses, logits = batch_generation( self.model, queries, self.mini_batch_size, self.tokenizer.pad_token_id, self.generation_config ) max_length_query = query_responses.shape[1] max_length_logits = max_length_query - context_length assert max_length_query > context_length assert query_responses.shape == (bs, max_length_query) assert logits.shape == (bs, max_length_logits, self.model.config.vocab_size) def test_single_batch_generation(self): batch = [ self.tokenizer.apply_chat_template(example[:-1], add_generation_prompt=True, tokenize=False) for example in self.examples ] queries = self.tokenizer(batch, padding=True, return_tensors="pt")["input_ids"].to(self.device) bs, context_length = queries.shape query_responses, logits = batch_generation( self.model, queries, bs, self.tokenizer.pad_token_id, self.generation_config ) max_length_query = query_responses.shape[1] max_length_logits = max_length_query - context_length assert max_length_query > context_length assert query_responses.shape == (bs, max_length_query) assert logits.shape == (bs, max_length_logits, self.model.config.vocab_size) class BaseTester: class VHeadModelTester(TrlTestCase): all_model_names = None trl_model_class = None transformers_model_class = None def setup_method(self): self.device = "cuda" if torch.cuda.is_available() else "cpu" def test_value_head(self): r""" Test if the v-head is added to the model successfully """ for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) assert hasattr(model, "v_head") def test_value_head_shape(self): r""" Test if the v-head has the correct shape """ for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) assert model.v_head.summary.weight.shape[0] == 1 def test_value_head_init_random(self): r""" Test if the v-head has been randomly initialized. We can check that by making sure the bias is different than zeros by default. """ for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) assert not torch.allclose(model.v_head.summary.bias, torch.zeros_like(model.v_head.summary.bias)) def test_value_head_not_str(self): r""" Test if the v-head is added to the model successfully, by passing a non `PretrainedModel` as an argument to `from_pretrained`. """ for model_name in self.all_model_names: pretrained_model = self.transformers_model_class.from_pretrained(model_name) model = self.trl_model_class.from_pretrained(pretrained_model) assert hasattr(model, "v_head") def test_from_save_trl(self): """ Test if the model can be saved and loaded from a directory and get the same weights, including the additional modules (e.g. v_head) """ for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) model.save_pretrained(self.tmp_dir) model_from_save = self.trl_model_class.from_pretrained(self.tmp_dir) # Check if the weights are the same for key in model_from_save.state_dict(): torch.testing.assert_close(model_from_save.state_dict()[key], model.state_dict()[key]) def test_from_save_trl_sharded(self): """ Test if the model can be saved and loaded from a directory and get the same weights - sharded case """ for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) model.save_pretrained(self.tmp_dir) model_from_save = self.trl_model_class.from_pretrained(self.tmp_dir) # Check if the weights are the same for key in model_from_save.state_dict(): torch.testing.assert_close(model_from_save.state_dict()[key], model.state_dict()[key]) def test_from_save_transformers_sharded(self): """ Test if the model can be saved and loaded using transformers and get the same weights - sharded case """ for model_name in self.all_model_names: transformers_model = self.trl_model_class.transformers_parent_class.from_pretrained(model_name) trl_model = self.trl_model_class.from_pretrained(model_name) trl_model.save_pretrained(self.tmp_dir, max_shard_size="1MB") transformers_model_from_save = self.trl_model_class.transformers_parent_class.from_pretrained( self.tmp_dir ) # Check if the weights are the same for key in transformers_model.state_dict(): torch.testing.assert_close( transformers_model_from_save.state_dict()[key], transformers_model.state_dict()[key] ) def test_from_save_transformers(self): """ Test if the model can be saved and loaded using transformers and get the same weights. We override the test of the super class to check if the weights are the same. """ for model_name in self.all_model_names: transformers_model = self.trl_model_class.transformers_parent_class.from_pretrained(model_name) trl_model = self.trl_model_class.from_pretrained(model_name) trl_model.save_pretrained(self.tmp_dir) transformers_model_from_save = self.trl_model_class.transformers_parent_class.from_pretrained( self.tmp_dir ) # Check if the weights are the same for key in transformers_model.state_dict(): torch.testing.assert_close( transformers_model_from_save.state_dict()[key], transformers_model.state_dict()[key] ) # Check if the trl model has the same keys as the transformers model # except the v_head for key in trl_model.state_dict(): if "v_head" not in key: assert key in transformers_model.state_dict() # check if the weights are the same torch.testing.assert_close(trl_model.state_dict()[key], transformers_model.state_dict()[key]) # check if they have the same modules assert set(transformers_model_from_save.state_dict().keys()) == set( transformers_model.state_dict().keys() ) class TestCausalLMValueHeadModel(BaseTester.VHeadModelTester, TrlTestCase): """ Testing suite for v-head models. """ all_model_names = ALL_CAUSAL_LM_MODELS trl_model_class = AutoModelForCausalLMWithValueHead transformers_model_class = AutoModelForCausalLM def teardown_method(self): # free memory gc.collect() def test_inference(self): r""" Test if the model can be used for inference and outputs 3 values - logits, loss, and value states """ EXPECTED_OUTPUT_SIZE = 3 for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name).to(self.device) input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) outputs = model(input_ids) # Check if the outputs are of the right size - here # we always output 3 values - logits, loss, and value states assert len(outputs) == EXPECTED_OUTPUT_SIZE def test_dropout_config(self): r""" Test if we instantiate a model by adding `summary_drop_prob` to the config it will be added to the v_head """ for model_name in self.all_model_names: pretrained_model = self.transformers_model_class.from_pretrained(model_name) pretrained_model.config.summary_dropout_prob = 0.5 model = self.trl_model_class.from_pretrained(pretrained_model) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == pretrained_model.config.summary_dropout_prob def test_dropout_kwargs(self): r""" Test if we instantiate a model by adding `summary_drop_prob` to the config it will be added to the v_head """ for model_name in self.all_model_names: v_head_kwargs = {"summary_dropout_prob": 0.5} model = self.trl_model_class.from_pretrained(model_name, **v_head_kwargs) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == 0.5 model = self.trl_model_class.from_pretrained(model_name, summary_dropout_prob=0.5) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == 0.5 @pytest.mark.parametrize("model_name", ALL_CAUSAL_LM_MODELS) def test_generate(self, model_name): r""" Test if `generate` works for every model """ generation_config = GenerationConfig(max_new_tokens=9) model = self.trl_model_class.from_pretrained(model_name).to(self.device) input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) # Just check if the generation works _ = model.generate(input_ids, generation_config=generation_config) def test_transformers_bf16_kwargs(self): r""" Test if the transformers kwargs are correctly passed. Here we check that loading a model in half precision works as expected, i.e. the weights of the `pretrained_model` attribute is loaded in half precision and you can run a dummy forward pass without any issue. """ for model_name in self.all_model_names: trl_model = self.trl_model_class.from_pretrained(model_name, dtype=torch.bfloat16).to(self.device) lm_head_namings = ["lm_head", "embed_out", "output_layer"] assert any(hasattr(trl_model.pretrained_model, lm_head_naming) for lm_head_naming in lm_head_namings), ( "Can't test the model because it doesn't have any of the expected lm_head namings" ) for lm_head_naming in lm_head_namings: if hasattr(trl_model.pretrained_model, lm_head_naming): assert getattr(trl_model.pretrained_model, lm_head_naming).weight.dtype == torch.bfloat16 dummy_input = torch.LongTensor([[0, 1, 0, 1]]).to(self.device) # check dummy forward pass works in half precision _ = trl_model(dummy_input) @pytest.mark.skip(reason="This test needs to be run manually due to HF token issue.") def test_push_to_hub(self): for model_name in self.all_model_names: model = AutoModelForCausalLMWithValueHead.from_pretrained(model_name) if "sharded" in model_name: model.push_to_hub(model_name + "-ppo", use_auth_token=True, max_shard_size="1MB") else: model.push_to_hub(model_name + "-ppo", use_auth_token=True) model_from_pretrained = AutoModelForCausalLMWithValueHead.from_pretrained(model_name + "-ppo") # check all keys assert model.state_dict().keys() == model_from_pretrained.state_dict().keys() for name, param in model.state_dict().items(): ( torch.testing.assert_close(param, model_from_pretrained.state_dict()[name]), (f"Parameter {name} is not the same after push_to_hub and from_pretrained"), ) class TestSeq2SeqValueHeadModel(BaseTester.VHeadModelTester, TrlTestCase): """ Testing suite for v-head models. """ all_model_names = ALL_SEQ2SEQ_MODELS trl_model_class = AutoModelForSeq2SeqLMWithValueHead transformers_model_class = AutoModelForSeq2SeqLM def teardown_method(self): # free memory gc.collect() def test_inference(self): r""" Test if the model can be used for inference and outputs 3 values - logits, loss, and value states """ EXPECTED_OUTPUT_SIZE = 3 for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name).to(self.device) input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) decoder_input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) outputs = model(input_ids, decoder_input_ids=decoder_input_ids) # Check if the outputs are of the right size - here # we always output 3 values - logits, loss, and value states assert len(outputs) == EXPECTED_OUTPUT_SIZE def test_dropout_config(self): r""" Test if we instantiate a model by adding `summary_drop_prob` to the config it will be added to the v_head """ for model_name in self.all_model_names: pretrained_model = self.transformers_model_class.from_pretrained(model_name) pretrained_model.config.summary_dropout_prob = 0.5 model = self.trl_model_class.from_pretrained(pretrained_model) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == pretrained_model.config.summary_dropout_prob def test_dropout_kwargs(self): r""" Test if we instantiate a model by adding `summary_drop_prob` to the config it will be added to the v_head """ for model_name in self.all_model_names: v_head_kwargs = {"summary_dropout_prob": 0.5} model = self.trl_model_class.from_pretrained(model_name, **v_head_kwargs) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == 0.5 model = self.trl_model_class.from_pretrained(model_name, summary_dropout_prob=0.5) # Check if v head of the model has the same dropout as the config assert model.v_head.dropout.p == 0.5 @pytest.mark.parametrize("model_name", ALL_SEQ2SEQ_MODELS) def test_generate(self, model_name): r""" Test if `generate` works for every model """ generation_config = GenerationConfig(max_new_tokens=9) model = self.trl_model_class.from_pretrained(model_name).to(self.device) input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) decoder_input_ids = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], device=self.device) # Just check if the generation works _ = model.generate(input_ids, decoder_input_ids=decoder_input_ids, generation_config=generation_config) @pytest.mark.skip(reason="This test needs to be run manually due to HF token issue.") def test_push_to_hub(self): for model_name in self.all_model_names: model = self.trl_model_class.from_pretrained(model_name) if "sharded" in model_name: model.push_to_hub(model_name + "-ppo", use_auth_token=True, max_shard_size="1MB") else: model.push_to_hub(model_name + "-ppo", use_auth_token=True) model_from_pretrained = self.trl_model_class.from_pretrained(model_name + "-ppo") # check all keys assert model.state_dict().keys() == model_from_pretrained.state_dict().keys() for name, param in model.state_dict().items(): ( torch.testing.assert_close(param, model_from_pretrained.state_dict()[name]), (f"Parameter {name} is not the same after push_to_hub and from_pretrained"), ) def test_transformers_bf16_kwargs(self): r""" Test if the transformers kwargs are correctly passed. Here we check that loading a model in half precision works as expected, i.e. the weights of the `pretrained_model` attribute is loaded in half precision and you can run a dummy forward pass without any issue. """ for model_name in self.all_model_names: trl_model = self.trl_model_class.from_pretrained(model_name, dtype=torch.bfloat16).to(self.device) lm_head_namings = self.trl_model_class.lm_head_namings assert any(hasattr(trl_model.pretrained_model, lm_head_naming) for lm_head_naming in lm_head_namings) for lm_head_naming in lm_head_namings: if hasattr(trl_model.pretrained_model, lm_head_naming): assert getattr(trl_model.pretrained_model, lm_head_naming).weight.dtype == torch.bfloat16 dummy_input = torch.LongTensor([[0, 1, 0, 1]]).to(self.device) # check dummy forward pass works in half precision _ = trl_model(input_ids=dummy_input, decoder_input_ids=dummy_input) @require_peft class TestPeftModel(TrlTestCase): def setup_method(self): self.causal_lm_model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.lora_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) def test_create_peft_model(self): r""" Simply creates a peft model and checks that it can be loaded. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) _ = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model) def test_peft_requires_grad(self): r""" Check that the value head of the returned model has requires_grad=True. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) model = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model) # Check that the value head has requires_grad=True assert model.v_head.summary.weight.requires_grad def test_check_peft_model_nb_trainable_params(self): r""" Check that the number of trainable parameters is correct. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) model = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) assert nb_trainable_params == 905 # Check that the number of trainable param for the non-peft model is correct non_peft_model = AutoModelForCausalLMWithValueHead.from_pretrained(self.causal_lm_model_id) nb_trainable_params = sum(p.numel() for p in non_peft_model.parameters() if p.requires_grad) assert nb_trainable_params == 2428641 def test_create_peft_model_from_config(self): r""" Simply creates a peft model and checks that it can be loaded. """ trl_model = AutoModelForCausalLMWithValueHead.from_pretrained( self.causal_lm_model_id, peft_config=self.lora_config ) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in trl_model.parameters() if p.requires_grad) assert nb_trainable_params == 905 causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) trl_model = AutoModelForCausalLMWithValueHead.from_pretrained(causal_lm_model, peft_config=self.lora_config) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in trl_model.parameters() if p.requires_grad) assert nb_trainable_params == 905 @require_bitsandbytes @require_torch_gpu_if_bnb_not_multi_backend_enabled def test_create_bnb_peft_model_from_config(self): r""" Simply creates a peft model and checks that it can be loaded. """ from bitsandbytes.nn import Linear8bitLt from transformers import BitsAndBytesConfig trl_model = AutoModelForCausalLMWithValueHead.from_pretrained( self.causal_lm_model_id, peft_config=self.lora_config, quantization_config=BitsAndBytesConfig(load_in_8bit=True), ) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in trl_model.parameters() if p.requires_grad) assert nb_trainable_params == 905 assert isinstance(trl_model.pretrained_model.model.model.layers[0].mlp.gate_proj, Linear8bitLt) causal_lm_model = AutoModelForCausalLM.from_pretrained( self.causal_lm_model_id, quantization_config=BitsAndBytesConfig(load_in_8bit=True), device_map="auto" ) trl_model = AutoModelForCausalLMWithValueHead.from_pretrained(causal_lm_model, peft_config=self.lora_config) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in trl_model.parameters() if p.requires_grad) assert nb_trainable_params == 905 assert isinstance(trl_model.pretrained_model.model.model.layers[0].mlp.gate_proj, Linear8bitLt) def test_save_pretrained_peft(self): r""" Check that the model can be saved and loaded properly. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) model = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model) model.save_pretrained(self.tmp_dir) # check that the files `adapter_model.safetensors` and `adapter_config.json` are in the directory assert os.path.isfile(f"{self.tmp_dir}/adapter_model.safetensors"), ( f"{self.tmp_dir}/adapter_model.safetensors does not exist" ) assert os.path.exists(f"{self.tmp_dir}/adapter_config.json"), ( f"{self.tmp_dir}/adapter_config.json does not exist" ) # check also for `pytorch_model.bin` and make sure it only contains `v_head` weights assert os.path.exists(f"{self.tmp_dir}/pytorch_model.bin"), f"{self.tmp_dir}/pytorch_model.bin does not exist" # check that only keys that starts with `v_head` are in the dict maybe_v_head = torch.load(f"{self.tmp_dir}/pytorch_model.bin", weights_only=True) assert all(k.startswith("v_head") for k in maybe_v_head.keys()), ( f"keys in {self.tmp_dir}/pytorch_model.bin do not start with `v_head`" ) model_from_pretrained = AutoModelForCausalLMWithValueHead.from_pretrained(self.tmp_dir) # check all the weights are the same for p1, p2 in zip(model.named_parameters(), model_from_pretrained.named_parameters(), strict=True): torch.testing.assert_close(p1[1], p2[1]), f"{p1[0]} != {p2[0]}" def test_load_pretrained_peft(self): r""" Check that the model saved with peft class interface can be loaded properly. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) model = AutoModelForCausalLMWithValueHead.from_pretrained(pretrained_model) pretrained_model.save_pretrained(self.tmp_dir) model_from_pretrained = AutoModelForCausalLMWithValueHead.from_pretrained(self.tmp_dir) # check that the files `adapter_model.safetensors` and `adapter_config.json` are in the directory assert os.path.isfile(f"{self.tmp_dir}/adapter_model.safetensors"), ( f"{self.tmp_dir}/adapter_model.safetensors does not exist" ) assert os.path.exists(f"{self.tmp_dir}/adapter_config.json"), ( f"{self.tmp_dir}/adapter_config.json does not exist" ) # check all the weights are the same for p1, p2 in zip(model.named_parameters(), model_from_pretrained.named_parameters(), strict=True): if p1[0] not in ["v_head.summary.weight", "v_head.summary.bias"]: torch.testing.assert_close(p1[1], p2[1]), f"{p1[0]} != {p2[0]}" def test_continue_training_peft_model(self): r""" Load peft and checks that it can continue training. """ causal_lm_model = AutoModelForCausalLM.from_pretrained(self.causal_lm_model_id) pretrained_model = get_peft_model(causal_lm_model, self.lora_config) pretrained_model.save_pretrained(self.tmp_dir) # set is_trainable to True model = AutoModelForCausalLMWithValueHead.from_pretrained(self.tmp_dir, is_trainable=True) # Check that the number of trainable parameters is correct nb_trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad) assert nb_trainable_params == 905 class TestCore(TrlTestCase): """ A wrapper class for testing core utils functions """ def setup_method(self): self.test_input = torch.Tensor([1, 2, 3, 4]) self.test_mask = torch.Tensor([0, 1, 1, 0]) self.test_input_unmasked = self.test_input[1:3] def test_masked_mean(self): assert torch.mean(self.test_input_unmasked) == masked_mean(self.test_input, self.test_mask) def test_masked_var(self): assert torch.var(self.test_input_unmasked) == masked_var(self.test_input, self.test_mask) def test_masked_whiten(self): def whiten(values: torch.Tensor) -> torch.Tensor: mean, var = torch.mean(values), torch.var(values) return (values - mean) * torch.rsqrt(var + 1e-8) whiten_unmasked = whiten(self.test_input_unmasked) whiten_masked = masked_whiten(self.test_input, self.test_mask)[1:3] diffs = (whiten_unmasked - whiten_masked).sum() assert abs(diffs.item()) < 0.00001 class TestPPOTrainer(TrlTestCase): def setup_method(self): # Set up the models and tokenizer using the test model self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.ref_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id, padding_side="left") self.tokenizer.add_special_tokens({"pad_token": "[PAD]"}) # Add reward and value models as in ppo.py reward_model_id = "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" self.value_model = AutoModelForSequenceClassification.from_pretrained(reward_model_id, num_labels=1) self.reward_model = AutoModelForSequenceClassification.from_pretrained(reward_model_id, num_labels=1) # Load dataset raw_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") def tokenize(example, tokenizer): tokenized = tokenizer(text=example["prompt"]) if tokenizer.eos_token_id is not None and tokenized["input_ids"][-1] != tokenizer.eos_token_id: tokenized["input_ids"] = tokenized["input_ids"] + [tokenizer.eos_token_id] tokenized["attention_mask"] = tokenized["attention_mask"] + [1] return tokenized self.raw_dataset = raw_dataset.map(tokenize, fn_kwargs={"tokenizer": self.tokenizer}, remove_columns="prompt") def test_basic_training(self): """Test basic PPO training configuration and verify model updates.""" # Capture initial weights initial_critic_weights = {} initial_policy_weights = {} for name, param in self.value_model.named_parameters(): initial_critic_weights[name] = param.clone().detach() for name, param in self.model.named_parameters(): initial_policy_weights[name] = param.clone().detach() # Configure training args similar to example script training_args = PPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=4, per_device_eval_batch_size=2, num_ppo_epochs=2, # Decrease number of PPO epochs to speed up test report_to="none", ) # Create trainer trainer = PPOTrainer( args=training_args, processing_class=self.tokenizer, model=self.model, ref_model=self.ref_model, reward_model=self.reward_model, value_model=self.value_model, train_dataset=self.raw_dataset["train"], eval_dataset=self.raw_dataset["test"], ) # Train trainer.train() # Check if critic weights have been updated critic_weights_updated = False for name, param in trainer.model.value_model.named_parameters(): if not torch.allclose(initial_critic_weights[name], param.to("cpu")): critic_weights_updated = True break # Check if policy weights have been updated policy_weights_updated = False for name, param in trainer.model.policy.named_parameters(): if not torch.allclose(initial_policy_weights[name], param.to("cpu")): policy_weights_updated = True break assert critic_weights_updated, "Critic weights were not updated during training" assert policy_weights_updated, "Policy weights were not updated during training" @require_peft def test_peft_training(self): """Test PPO training with PEFT configuration and verify model updates.""" # Capture initial weights initial_critic_weights = {} initial_policy_weights = {} for name, param in self.value_model.named_parameters(): initial_critic_weights[name] = param.clone().detach() for name, param in self.model.named_parameters(): initial_policy_weights[name] = param.clone().detach() # Configure training args training_args = PPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=4, per_device_eval_batch_size=2, num_ppo_epochs=2, # Decrease number of PPO epochs to speed up test report_to="none", ) # Configure PEFT peft_config = LoraConfig( r=32, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # Create trainer with PEFT trainer = PPOTrainer( args=training_args, processing_class=self.tokenizer, model=self.model, ref_model=None, reward_model=self.reward_model, value_model=self.value_model, train_dataset=self.raw_dataset["train"], eval_dataset=self.raw_dataset["test"], peft_config=peft_config, ) # Train trainer.train() # Check if critic weights have been updated critic_weights_updated = False for name, param in trainer.model.value_model.named_parameters(): if name in initial_critic_weights and not torch.allclose(initial_critic_weights[name], param.to("cpu")): critic_weights_updated = True break # Check if policy weights have been updated - for PEFT we check the LoRA weights policy_weights_updated = False for name, param in trainer.model.policy.named_parameters(): if "lora" in name.lower() and param.requires_grad: # Only check LoRA weights # New weights should be non-zero if they've been updated if not torch.allclose(param, torch.zeros_like(param)): policy_weights_updated = True break assert critic_weights_updated, "Critic weights were not updated during training" assert policy_weights_updated, "Policy LoRA weights were not updated during training" ================================================ FILE: tests/experimental/test_prm_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest.mock import MagicMock import numpy as np import pytest import torch from datasets import Dataset, load_dataset from transformers import AutoModelForTokenClassification, AutoTokenizer, PreTrainedTokenizerBase from transformers.utils import is_peft_available from trl.experimental.prm import PRMConfig, PRMTrainer from trl.experimental.prm.prm_trainer import compute_accuracy from ..testing_utils import TrlTestCase, require_peft if is_peft_available(): from peft import LoraConfig, TaskType class TestComputeAccuracy(TrlTestCase): def test_token_classification_task(self): eval_pred = ( np.array( [ [[0.1, 0.9], [0.8, 0.2]], # Batch 1 [[0.3, 0.7], [0.6, 0.4]], # Batch 2 ] ), np.array([[0, 1], [1, 0]]), ) expected_accuracy = 0.5 # 2 matches, 2 mismatches result = compute_accuracy(eval_pred) assert round(abs(result["accuracy"] - expected_accuracy), 7) == 0 def test_token_classification_task_with_ignored_tokens_0(self): eval_pred = ( np.array( [ [[0.1, 0.9], [0.8, 0.2]], # Batch 1 [[0.3, 0.7], [0.6, 0.4]], # Batch 2 ] ), np.array([[1, 0], [1, -100]]), ) expected_accuracy = 1.0 # All non-ignored tokens match result = compute_accuracy(eval_pred) assert round(abs(result["accuracy"] - expected_accuracy), 7) == 0 def test_token_classification_task_with_ignored_tokens_1(self): eval_pred = ( np.array( [ [[0.1, 0.9], [0.8, 0.2]], # Batch 1 [[0.3, 0.7], [0.6, 0.4]], # Batch 2 ] ), np.array([[1, 1], [0, -100]]), ) expected_accuracy = 1 / 3 # 1 match, 2 mismatch, 1 ignored result = compute_accuracy(eval_pred) assert round(abs(result["accuracy"] - expected_accuracy), 7) == 0 def test_rewards_comparison_task(self, caplog): eval_pred = ( np.array( [ [0.9, 0.1], # Batch 1 [0.6, 0.4], # Batch 2 [0.5, 0.5], # Batch 3 (equal) ] ), np.array([0, 1, 1]), ) expected_accuracy = 0.5 # 1 match, 1 mismatch, 1 equal (ignored) with caplog.at_level("WARNING", logger="trl.trainer.utils"): result = compute_accuracy(eval_pred) assert round(abs(result["accuracy"] - expected_accuracy), 7) == 0 expected_warning = ( "There are 1 out of 3 instances where the predictions for both options are equal. " "These instances are ignored in the accuracy computation." ) assert expected_warning in caplog.text class TestTokenizeRow(TrlTestCase): def setup_method(self): # Set up the mock tokenizer with specific behaviors self.tokenizer = MagicMock(spec=PreTrainedTokenizerBase) self.tokenizer.bos_token_id = 0 self.tokenizer.eos_token_id = 2 def mock_encode(text, add_special_tokens): token_map = { "Which number is larger, 9.8 or 9.11?": [465, 6766, 318, 298], "11 is greater than 8.": [4, 322, 12], "Hence, 9.11 > 9.8.": [4995, 11, 22], "\n": [1030], "\n\n": [1030, 1030], } return token_map[text] def mock_tokenizer_call(text, add_special_tokens): return {"input_ids": mock_encode(text, add_special_tokens)} self.tokenizer.encode.side_effect = mock_encode self.tokenizer.side_effect = mock_tokenizer_call def test_tokenize_row_no_truncation(self): # Define the input features features = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], "labels": [True, False], } # Call the method with no truncation result = PRMTrainer.tokenize_row( features=features, tokenizer=self.tokenizer, step_separator="\n", max_length=None, max_completion_length=None, train_on_last_step_only=False, is_eval=False, ) assert result == { "input_ids": [0, 465, 6766, 318, 298, 4, 322, 12, 1030, 4995, 11, 22, 1030], "labels": [-100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100, -100, 0], } def test_tokenize_row_train_on_last_step_only(self): # Define the input features features = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], "labels": [True, False], } result = PRMTrainer.tokenize_row( features=features, tokenizer=self.tokenizer, step_separator="\n", max_length=None, max_completion_length=None, train_on_last_step_only=True, is_eval=False, ) assert result == { "input_ids": [0, 465, 6766, 318, 298, 4, 322, 12, 1030, 4995, 11, 22, 1030], "labels": [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 0], } def test_tokenize_row_completion_truncation(self): # Define the input features features = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], "labels": [True, False], } # Call the method with truncation on the completion result = PRMTrainer.tokenize_row( features=features, tokenizer=self.tokenizer, step_separator="\n", max_length=None, max_completion_length=6, train_on_last_step_only=False, is_eval=False, ) assert result == { "input_ids": [0, 465, 6766, 318, 298, 4, 322, 12, 1030, 4995, 11], "labels": [-100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100], } def test_tokenize_row_prompt_completion_truncation(self): # Define the input features features = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], "labels": [True, False], } # Call the method with truncation on the prompt and completion result = PRMTrainer.tokenize_row( features=features, tokenizer=self.tokenizer, step_separator="\n", max_length=9, max_completion_length=None, train_on_last_step_only=False, is_eval=False, ) assert result == { "input_ids": [0, 465, 6766, 318, 298, 4, 322, 12, 1030], "labels": [-100, -100, -100, -100, -100, -100, -100, -100, 1], } def test_tokenize_row_multi_token_separator(self): # Define the input features features = { "prompt": "Which number is larger, 9.8 or 9.11?", "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], "labels": [True, False], } # Call the method using multiple tokens as step_separator result = PRMTrainer.tokenize_row( features=features, tokenizer=self.tokenizer, step_separator="\n\n", max_length=None, max_completion_length=None, train_on_last_step_only=False, is_eval=False, ) assert result == { "input_ids": [0, 465, 6766, 318, 298, 4, 322, 12, 1030, 1030, 4995, 11, 22, 1030, 1030], "labels": [-100, -100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100, -100, -100, 0], } class TestPRMTrainer(TrlTestCase): def setup_method(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForTokenClassification.from_pretrained(model_id, dtype="float32") self.tokenizer = AutoTokenizer.from_pretrained(model_id) @pytest.mark.parametrize("train_on_last_step_only", [True, False]) def test_train_full(self, train_on_last_step_only): dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_stepwise_supervision", split="train") training_args = PRMConfig( output_dir=self.tmp_dir, report_to="none", train_on_last_step_only=train_on_last_step_only, ) trainer = PRMTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12) def test_train_full_pretokenized(self): dummy_dataset = Dataset.from_dict( { "labels": [ [-100, -100, -100, -100, -100, -100, -100, -100, -100, 0, -100, -100, 1], [-100, -100, -100, -100, -100, -100, -100, -100, 0, -100, -100, 1, -100, -100, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 0, -100, -100, 1], [-100, -100, -100, -100, -100, -100, -100, 1, -100, -100, 1], [-100, -100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, -100, 1], [-100, -100, -100, -100, -100, -100, -100, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100, -100, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, 0, -100, -100, 0], [-100, -100, -100, -100, -100, -100, 0, -100, -100, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 1], [-100, -100, -100, -100, -100, -100, 0], [-100, -100, -100, -100, -100, -100, -100, -100, 1], [-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 0], ], "input_ids": [ [46518, 374, 2664, 1091, 11, 1077, 752, 1744, 1112, 198, 27261, 13, 198], [98923, 374, 2664, 1091, 11, 315, 3308, 11, 198, 17995, 13, 198, 1576, 31273, 12850, 13, 198], [16374, 374, 2664, 1091, 1112, 1077, 594, 2506, 432, 6770, 11, 198, 6351, 13, 198], [31137, 374, 2664, 1091, 979, 4362, 11, 198, 16965, 13, 198], [31019, 374, 2664, 1091, 304, 3793, 315, 5944, 11, 198, 24034, 13, 198], [98491, 374, 2664, 1091, 1112, 5310, 369, 91494, 13, 198], [4418, 2897, 14579, 5310, 979, 3800, 1349, 432, 13, 198], [20366, 5048, 7629, 944, 3281, 3322, 11, 7241, 1112, 198, 807, 1795, 279, 5601, 13, 198], [15802, 14976, 487, 33327, 1045, 31787, 63443, 11, 198, 52400, 13, 198], [13877, 1265, 2581, 1494, 49394, 11, 198, 7241, 20975, 91681, 13, 198], [641, 279, 3579, 315, 71768, 11, 25066, 279, 61361, 311, 7942, 13, 198], [7039, 374, 2664, 1091, 2937, 13, 198], [26155, 374, 3545, 2664, 1091, 34933, 26537, 13, 198], [2679, 279, 8129, 374, 4135, 311, 10339, 11, 432, 2578, 387, 264, 1661, 2884, 13, 198], ], } ) training_args = PRMConfig(output_dir=self.tmp_dir, report_to="none") trainer = PRMTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if param.sum() != 0: # ignore 0 biases assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12) @require_peft def test_train_lora(self): peft_config = LoraConfig( task_type=TaskType.TOKEN_CLS, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1, ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_stepwise_supervision", split="train") training_args = PRMConfig(output_dir=self.tmp_dir, max_steps=3, report_to="none") trainer = PRMTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset, peft_config=peft_config, ) previous_trainable_params = {} previous_non_trainable_params = {} # due to a change in the way the modules to save are dealt in PEFT. trainable_params_name = ["lora", "modules_to_save"] # check gradients are not None for n, param in trainer.model.named_parameters(): if any(t in n for t in trainable_params_name): previous_trainable_params[n] = param.clone() else: previous_non_trainable_params[n] = param.clone() trainer.train() assert trainer.state.log_history[(-1)]["train_loss"] is not None # Check that the parameters have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param, atol=1e-12, rtol=1e-12) # Check that the non trainable parameters have not changed for n, param in previous_non_trainable_params.items(): new_param = trainer.model.get_parameter(n) torch.testing.assert_close(param, new_param, atol=1e-12, rtol=1e-12) def test_tags(self): dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_stepwise_supervision", split="train") training_args = PRMConfig(output_dir=self.tmp_dir, report_to="none") trainer = PRMTrainer( model=self.model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset ) assert trainer.model.model_tags == trainer._tag_names ================================================ FILE: tests/experimental/test_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from datasets import load_dataset from transformers import AutoTokenizer from trl.experimental.utils import DataCollatorForChatML from ..testing_utils import TrlTestCase class TestDataCollatorForChatML(TrlTestCase): def setup_method(self): # Initialize the tokenizer self.tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") if self.tokenizer.pad_token is None: self.tokenizer.pad_token = self.tokenizer.eos_token # Define token IDs self.bos_token_id = self.tokenizer.bos_token_id if self.tokenizer.bos_token_id is not None else 1 self.eos_token_id = self.tokenizer.eos_token_id if self.tokenizer.eos_token_id is not None else 2 # Token ID for "true", the last assistant's response in the example: self.ignore_index = -100 self.max_length = 1024 self.messages_key = "messages" # Example input dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") self.examples = dataset.to_list() # Initialize the data collator self.collator = DataCollatorForChatML( tokenizer=self.tokenizer, max_length=self.max_length, ignore_index=self.ignore_index, ) def test_data_collator_for_chatml(self): # Process the data data = self.collator(self.examples) # Verify basic shapes and types assert "input_ids" in data assert "attention_mask" in data assert "labels" in data assert "prompts" in data assert "prompt_attention_mask" in data # Decode input_ids and labels for verification input_ids = data["input_ids"][0].tolist() labels = data["labels"][0].tolist() prompt_only = data["prompts"][0].tolist() # Get the last assistant's response for comparison last_message = self.examples[0][self.messages_key][-1] assert last_message["role"] == "assistant", "Last message should be from assistant" last_assistant_response = last_message["content"] # Verify that input_ids contain both prompt and response decoded_input = self.tokenizer.decode(input_ids) assert last_assistant_response in decoded_input, "Input should contain assistant's response" # Verify that prompts only contain the conversation up to the last response decoded_prompt = self.tokenizer.decode(prompt_only) assert last_assistant_response not in decoded_prompt, "Prompt should not contain assistant's response" # Verify labels are -100 for non-assistant parts prompt_length = len(prompt_only) assert all(label == self.ignore_index for label in labels[:prompt_length]), ( "Labels should be ignore_index for prompt tokens" ) # Verify labels match assistant response after prompt # Add a filter to remove any trailing tokens after the first <|im_end|> last_assistant_response_with_end = last_assistant_response + self.tokenizer.eos_token last_assistant_response_tokens = self.tokenizer.encode( last_assistant_response_with_end, add_special_tokens=False ) response_labels = [] for label in labels[prompt_length:]: if label == self.ignore_index: continue response_labels.append(label) if label == self.tokenizer.convert_tokens_to_ids("<|im_end|>"): break assert response_labels == last_assistant_response_tokens, "Labels should match assistant response tokens" # Verify there isn't a generation prompt at the end generation_prompt = "<|im_start|>assistant" assert not decoded_input.strip().endswith(generation_prompt), ( f"Input should not end with generation prompt '{generation_prompt}'" ) assert response_labels == last_assistant_response_tokens, "Labels should match assistant response tokens" ================================================ FILE: tests/experimental/test_winrate_callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig, Trainer, TrainingArguments from transformers.utils import is_peft_available from trl.experimental.judges import BasePairwiseJudge from trl.experimental.winrate_callback import WinRateCallback from ..testing_utils import TrlTestCase, require_peft if is_peft_available(): from peft import LoraConfig class HalfPairwiseJudge(BasePairwiseJudge): """Naive pairwise judge that always returns [1, 0] for two prompts""" def judge(self, prompts, completions, shuffle_order=True, return_scores=False): # just check that the batch size is 2 assert len(prompts) == 2 if return_scores: return [0.3, 0.9] return [1, 0] class TrainerWithRefModel(Trainer): # This is a dummy class to test the callback. Compared to the Trainer class, it only has an additional # ref_model attribute def __init__(self, model, ref_model, args, train_dataset, eval_dataset, processing_class): super().__init__( model=model, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, ) # Prepare ref_model like TRL trainers do (DPOTrainer, GRPOTrainer, etc.) self.ref_model = self.accelerator.prepare_model(ref_model, evaluation_mode=True) class TestWinRateCallback(TrlTestCase): def setup_method(self): self.model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32" ) self.ref_model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer.pad_token = self.tokenizer.eos_token dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") dataset["train"] = dataset["train"].select(range(8)) self.expected_winrates = [ {"eval_win_rate": 0.5, "epoch": 0.0, "step": 0}, {"eval_win_rate": 0.5, "epoch": 0.5, "step": 2}, {"eval_win_rate": 0.5, "epoch": 1.0, "step": 4}, {"eval_win_rate": 0.5, "epoch": 1.5, "step": 6}, {"eval_win_rate": 0.5, "epoch": 2.0, "step": 8}, {"eval_win_rate": 0.5, "epoch": 2.5, "step": 10}, {"eval_win_rate": 0.5, "epoch": 3.0, "step": 12}, ] def tokenize_function(examples): out = self.tokenizer(examples["prompt"], padding="max_length", max_length=16, truncation=True) out["labels"] = out["input_ids"].copy() return out self.dataset = dataset.map(tokenize_function, batched=True) self.generation_config = GenerationConfig(max_length=32) self.judge = HalfPairwiseJudge() def test_basic(self): training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="none", ) trainer = TrainerWithRefModel( model=self.model, ref_model=self.ref_model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) win_rate_callback = WinRateCallback( judge=self.judge, trainer=trainer, generation_config=self.generation_config ) trainer.add_callback(win_rate_callback) trainer.train() winrate_history = [h for h in trainer.state.log_history if "eval_win_rate" in h] for history_row, expected_row in zip(winrate_history, self.expected_winrates, strict=True): assert all(key in history_row and history_row[key] == expected_row[key] for key in expected_row) def test_without_ref_model(self): # Same as before, but without the ref_model attribute. It should use the model attribute instead training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="none", ) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) win_rate_callback = WinRateCallback( judge=self.judge, trainer=trainer, generation_config=self.generation_config ) trainer.add_callback(win_rate_callback) trainer.train() winrate_history = [h for h in trainer.state.log_history if "eval_win_rate" in h] for history_row, expected_row in zip(winrate_history, self.expected_winrates, strict=True): assert all(key in history_row and history_row[key] == expected_row[key] for key in expected_row) def test_soft_judge(self): """Test that the soft judge functionality works correctly""" training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="none", ) trainer = TrainerWithRefModel( model=self.model, ref_model=self.ref_model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) win_rate_callback = WinRateCallback( judge=self.judge, trainer=trainer, generation_config=self.generation_config, use_soft_judge=True ) trainer.add_callback(win_rate_callback) trainer.train() # Expected values based on judge returning [0.3, 0.9] for each pair expected_soft_winrates = [ {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 0.0, "step": 0}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 0.5, "step": 2}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 1.0, "step": 4}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 1.5, "step": 6}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 2.0, "step": 8}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 2.5, "step": 10}, {"eval_avg_win_prob": 0.4, "eval_win_rate": 0.5, "epoch": 3.0, "step": 12}, ] winrate_history = [ {k: h[k] for k in ["eval_avg_win_prob", "eval_win_rate", "epoch", "step"]} for h in trainer.state.log_history if "eval_avg_win_prob" in h ] for history_row, expected_row in zip(winrate_history, expected_soft_winrates, strict=True): assert all(key in history_row and history_row[key] == expected_row[key] for key in expected_row) @require_peft def test_lora(self): peft_config = LoraConfig( r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) self.model.add_adapter(peft_config) training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="none", ) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) win_rate_callback = WinRateCallback( judge=self.judge, trainer=trainer, generation_config=self.generation_config ) trainer.add_callback(win_rate_callback) trainer.train() winrate_history = [h for h in trainer.state.log_history if "eval_win_rate" in h] for history_row, expected_row in zip(winrate_history, self.expected_winrates, strict=True): assert all(key in history_row and history_row[key] == expected_row[key] for key in expected_row) ================================================ FILE: tests/experimental/test_xpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer from transformers.utils import is_peft_available from trl.experimental.xpo import XPOConfig, XPOTrainer from ..testing_utils import TrlTestCase, require_llm_blender, require_peft from .testing_utils import RandomPairwiseJudge if is_peft_available(): from peft import LoraConfig, get_peft_model @pytest.mark.low_priority class TestXPOTrainer(TrlTestCase): def setup_method(self): self.model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" self.model = AutoModelForCausalLM.from_pretrained(self.model_id, dtype="float32") self.ref_model = AutoModelForCausalLM.from_pretrained(self.model_id) self.reward_model = AutoModelForSequenceClassification.from_pretrained(self.model_id, num_labels=1) self.tokenizer = AutoTokenizer.from_pretrained(self.model_id) self.tokenizer.pad_token = self.tokenizer.eos_token @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_xpo_trainer_training(self, config_name): training_args = XPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) trainer = XPOTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_with_peft(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = XPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = XPOTrainer( model=self.model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_with_peft_and_ref_model(self): lora_config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM") training_args = XPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, learning_rate=5.0e-7, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") trainer = XPOTrainer( model=self.model, ref_model=self.ref_model, reward_funcs=self.reward_model, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], peft_config=lora_config, ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] @require_peft def test_training_pre_pefted_model_implicit_ref(self): lora_config = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.1, bias="none", task_type="CAUSAL_LM") peft_model_instance = get_peft_model(self.model, lora_config) training_args = XPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=1, max_steps=2, learning_rate=5.0e-7, eval_strategy="no", report_to="none", remove_unused_columns=False, ) dummy_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only")["train"] trainer = XPOTrainer( model=peft_model_instance, ref_model=None, reward_funcs=self.reward_model, # Using reward_model to ensure _generate_completions is used as expected args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset, ) trainer.train() assert "train_loss" in trainer.state.log_history[-1] @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) @require_llm_blender def test_xpo_trainer_judge_training(self, config_name): training_args = XPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=2, max_steps=3, remove_unused_columns=False, gradient_accumulation_steps=1, learning_rate=9e-1, eval_strategy="steps", report_to="none", ) dummy_dataset = load_dataset("trl-internal-testing/zen", config_name) judge = RandomPairwiseJudge() trainer = XPOTrainer( model=self.model, ref_model=self.ref_model, judge=judge, args=training_args, processing_class=self.tokenizer, train_dataset=dummy_dataset["train"], eval_dataset=dummy_dataset["test"], ) trainer.train() # Check if training loss is available assert "train_loss" in trainer.state.log_history[-1] ================================================ FILE: tests/experimental/testing_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import random from trl.experimental.judges import BasePairwiseJudge class RandomPairwiseJudge(BasePairwiseJudge): """ Random pairwise judge, for testing purposes. """ def judge(self, prompts, completions, shuffle_order=True, return_scores=False): if not return_scores: return [random.randint(0, len(completion) - 1) for completion in completions] else: return [random.random() for _ in range(len(prompts))] ================================================ FILE: tests/test_activation_offloading.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch from torch import nn from transformers import AutoModelForCausalLM from transformers.testing_utils import torch_device from transformers.utils import is_peft_available from trl.models.activation_offloading import NoOpManager, OffloadActivations from .testing_utils import TrlTestCase, require_peft, require_torch_accelerator if is_peft_available(): from peft import LoraConfig, get_peft_model class TestActivationOffloading(TrlTestCase): @require_torch_accelerator @require_peft def test_offloading_with_peft_models(self) -> None: """Test that activation offloading works with PEFT models.""" model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id).to(torch_device) peft_config = LoraConfig( lora_alpha=16, lora_dropout=0.1, r=8, bias="none", task_type="CAUSAL_LM", ) model = get_peft_model(model, peft_config) inp = torch.randint(0, 100, (2, 10), device=torch_device) # First forward-backward pass without offloading torch.manual_seed(42) loss = model(inp, labels=inp).loss loss.backward() # Store gradients - only from trainable parameters grads_original = [] for name, param in model.named_parameters(): if param.requires_grad and param.grad is not None: grads_original.append((name, param.grad.clone())) # Reset gradients for p in model.parameters(): if p.grad is not None: p.grad = None # Second forward-backward pass with offloading torch.manual_seed(42) with OffloadActivations(): loss_c = model(inp, labels=inp).loss loss_c.backward() # Compare gradients - only trainable parameters for name_orig, grad_orig in grads_original: for name_param, param in model.named_parameters(): if name_param == name_orig and param.requires_grad and param.grad is not None: ( torch.testing.assert_close(grad_orig, param.grad, rtol=1e-4, atol=1e-5), (f"Gradient mismatch for {name_orig}"), ) @require_torch_accelerator def test_noop_manager_with_offloading(self): model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id).to(torch_device) inp = torch.randint(0, 100, (2, 10), device=torch_device) # Run with offloading but disable for specific section with OffloadActivations(): # First forward-backward with normal offloading torch.manual_seed(42) out1 = model(inp, labels=inp) out1.loss.backward() grads1 = [p.grad.clone() for p in model.parameters()] # Reset grads for p in model.parameters(): p.grad = None # Second forward-backward with NoOpManager with NoOpManager(): torch.manual_seed(42) out2 = model(inp, labels=inp) out2.loss.backward() grads2 = [p.grad.clone() for p in model.parameters()] # Gradients should match as NoOpManager should have prevented offloading for g1, g2 in zip(grads1, grads2, strict=True): torch.testing.assert_close(g1, g2, rtol=1e-4, atol=1e-5) @require_torch_accelerator def test_min_offload_size(self): """Test that tensors smaller than min_offload_size aren't offloaded""" model = nn.Sequential( nn.Linear(5, 5), # Small layer that shouldn't be offloaded nn.Linear(5, 1000), # Large layer that should be offloaded ).to(torch_device) inp = torch.randn(2, 5, device=torch_device) with OffloadActivations(min_offload_size=1000): out = model(inp) out.sum().backward() # The test passes if no errors occur, as we're mainly testing # that the logic handles both offloaded and non-offloaded tensors @require_torch_accelerator def test_real_hf_model(self): """Test with an actual HuggingFace model""" model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id).to(torch_device) # Create small input inp = torch.randint(0, 100, (2, 10), device=torch_device) # Baseline without offloading torch.manual_seed(42) out1 = model(inp, labels=inp).loss out1.backward() grads1 = [p.grad.clone() for p in model.parameters()] # Reset grads for p in model.parameters(): p.grad = None # With offloading with OffloadActivations(): torch.manual_seed(42) out2 = model(inp, labels=inp).loss out2.backward() grads2 = [p.grad.clone() for p in model.parameters()] # Check outputs and gradients match torch.testing.assert_close(out1, out2) for g1, g2 in zip(grads1, grads2, strict=True): torch.testing.assert_close(g1, g2) @require_torch_accelerator def test_tensor_deduplication(self): """Test that deduplication works correctly for tensors sharing storage""" class ModelWithViews(nn.Module): def __init__(self): super().__init__() self.linear = nn.Linear(100, 100) def forward(self, x): out = self.linear(x) view1 = out.view(-1) view2 = out.transpose(0, 1) return view1.sum() + view2.sum() model = ModelWithViews().to(torch_device) offload_ctx = OffloadActivations(min_offload_size=1) offload_ctx.update_model_params(model) x = torch.randn(10, 100, device=torch_device, requires_grad=True) with offload_ctx: loss = model(x) total_tensor_ids = offload_ctx.tensor_id assert total_tensor_ids > 0, "Should have created tensor IDs" # modified=True means offloaded to CPU, modified=False means kept on GPU (deduplicated) deduplicated_count = sum(1 for _, modified, _, _, _ in offload_ctx.tracker.values() if not modified) offloaded_count = sum(1 for _, modified, _, _, _ in offload_ctx.tracker.values() if modified) assert offloaded_count > 0, "Should have offloaded at least one tensor" assert deduplicated_count > 0, "Should have deduplicated at least one tensor (view)" unique_storages_offloaded = len(offload_ctx.storage_to_tensor_id) assert unique_storages_offloaded < total_tensor_ids, ( f"Deduplication should result in fewer storages ({unique_storages_offloaded}) " f"than total tensors ({total_tensor_ids})" ) loss.backward() @require_torch_accelerator def test_parameter_filtering(self): """Test that model parameters are filtered during offloading""" model = nn.Sequential(nn.Linear(10, 20), nn.Linear(20, 10)).to(torch_device) offload_ctx = OffloadActivations() offload_ctx.update_model_params(model) assert len(offload_ctx.param_storages) > 0, "Should have tracked parameter storages" param_ptrs = {p.data.untyped_storage().data_ptr() for p in model.parameters()} assert offload_ctx.param_storages == param_ptrs, "Tracked storages should match parameter storages" ================================================ FILE: tests/test_callbacks.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os from unittest.mock import call, patch from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig, Trainer, TrainingArguments from trl import BEMACallback, LogCompletionsCallback from .testing_utils import TrlTestCase, require_comet, require_wandb class TestLogCompletionsCallback(TrlTestCase): def setup_method(self): self.model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer.pad_token = self.tokenizer.eos_token dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") dataset["train"] = dataset["train"].select(range(8)) def tokenize_function(examples): out = self.tokenizer(examples["prompt"], padding="max_length", max_length=16, truncation=True) out["labels"] = out["input_ids"].copy() return out self.dataset = dataset.map(tokenize_function, batched=True) self.generation_config = GenerationConfig(max_length=32) @require_wandb def test_basic_wandb(self): import wandb training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="wandb", ) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) completions_callback = LogCompletionsCallback(trainer, self.generation_config, num_prompts=2) trainer.add_callback(completions_callback) trainer.train() # Get the current run completions_path = wandb.run.summary.completions["path"] json_path = os.path.join(wandb.run.dir, completions_path) with open(json_path) as f: completions = json.load(f) # Check that the columns are correct assert "step" in completions["columns"] assert "prompt" in completions["columns"] assert "completion" in completions["columns"] # Check that the prompt is in the log assert self.dataset["test"][0]["prompt"] in completions["data"][0] @require_comet def test_basic_comet(self): import comet_ml training_args = TrainingArguments( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=2, # evaluate every 2 steps per_device_train_batch_size=2, # 8 samples in total so 4 batches of 2 per epoch per_device_eval_batch_size=2, report_to="comet_ml", ) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], eval_dataset=self.dataset["test"], processing_class=self.tokenizer, ) completions_callback = LogCompletionsCallback(trainer, self.generation_config, num_prompts=2) trainer.add_callback(completions_callback) trainer.train() # close experiment to make sure all pending data are flushed experiment = comet_ml.get_running_experiment() assert experiment is not None experiment.end() # get experiment assets and check that all required tables was logged steps = len(self.dataset["train"]) + len(self.dataset["test"]) tables_logged = int(steps / 2) + 1 # +1 to include zero step api_experiment = comet_ml.APIExperiment(previous_experiment=experiment.id) tables = api_experiment.get_asset_list("dataframe") assert tables is not None assert len(tables) == tables_logged assert all(table["fileName"] == "completions.csv" for table in tables) class TestBEMACallback(TrlTestCase): def setup_method(self): self.model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") self.tokenizer.pad_token = self.tokenizer.eos_token dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") def tokenize_function(examples, tokenizer): out = tokenizer(examples["text"], padding="max_length", max_length=17) out["labels"] = out["input_ids"].copy() return out self.dataset = dataset.map( tokenize_function, fn_kwargs={"tokenizer": self.tokenizer}, remove_columns=["text"], batched=True ) def test_model_saved(self): """Test that BEMACallback saves the BEMA model.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=2) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() # Check that the BEMA model was saved and can be loaded bema_path = os.path.join(self.tmp_dir, "bema") assert os.path.isdir(bema_path), "BEMA directory was not created" AutoModelForCausalLM.from_pretrained(bema_path) def test_update_frequency_0(self): """Test that BEMA callback respects the update frequency.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=2) with patch.object(bema_callback, "_update_bema_weights") as mock_update: trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() # Total 9 steps (17 samples, batch size 8, 3 epochs). # BEMA starts after step 0 and updates every 2 steps → updates at 2, 4, 5, 8 assert mock_update.call_args_list == [call(2), call(4), call(6), call(8)] def test_update_frequency_1(self): """Test that BEMA callback respects the update frequency.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=3) with patch.object(bema_callback, "_update_bema_weights") as mock_update: trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() # Total 9 steps (17 samples, batch size 8, 3 epochs). # BEMA starts after step 0 and updates every 3 steps → updates at 3, 6, 9 assert mock_update.call_args_list == [call(3), call(6), call(9)] def test_update_frequency_2(self): """Test that BEMA callback respects the update frequency.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=2, update_after=3) with patch.object(bema_callback, "_update_bema_weights") as mock_update: trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() # Total 9 steps (17 samples, batch size 8, 3 epochs). # BEMA starts after step 3 and updates every 2 steps → updates at 5, 7, 9 assert mock_update.call_args_list == [call(5), call(7), call(9)] def test_no_bema(self): """Test that BEMACallback works without BEMA updates.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=2, bias_power=0.0) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() def test_no_ema(self): """Test that BEMACallback works without EMA updates.""" training_args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") bema_callback = BEMACallback(update_freq=2, ema_power=0.0) trainer = Trainer( model=self.model, args=training_args, train_dataset=self.dataset["train"], processing_class=self.tokenizer, callbacks=[bema_callback], ) trainer.train() ================================================ FILE: tests/test_chat_template_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap import pytest import transformers from packaging.version import Version from transformers import AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer from trl import clone_chat_template from trl.chat_template_utils import ( add_response_schema, get_training_chat_template, is_chat_template_prefix_preserving, parse_response, ) from .testing_utils import TrlTestCase, require_jmespath class TestCloneChatTemplate(TrlTestCase): def test_clone(self): # This tokenizer doesn't have a chat_template by default tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") # This one has a chat_template by default source = "trl-internal-testing/tiny-Qwen3ForCausalLM" _, modified_tokenizer, _ = clone_chat_template(model, tokenizer, source) # Check if special tokens are correctly set assert modified_tokenizer.eos_token == "<|im_end|>" def test_clone_with_resize(self): # This tokenizer doesn't have a chat_template by default tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") # This one has a chat_template by default source = "trl-internal-testing/tiny-Qwen3ForCausalLM" modified_model, modified_tokenizer, _ = clone_chat_template( model, tokenizer, source, resize_to_multiple_of=123 ) # Check that the input embeddings have been resized to a multiple of 123 assert (modified_model.vocab_size % 123) == 0 # Check that the input embeddings size matches the tokenizer vocabulary size assert model.vocab_size == len(modified_tokenizer.vocab) def test_clone_with_resize_and_extra_tokens_already_in_vocab(self): # This tokenizer doesn't have a chat_template by default tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") # This one has a chat_template by default source = "trl-internal-testing/tiny-Qwen3ForCausalLM" # This will add , , ... to the tokenizer modified_model, modified_tokenizer, _ = clone_chat_template( model, tokenizer, source, resize_to_multiple_of=123 ) # Try if we can resize a tokenizer that already has extra these extra tokens modified_model, modified_tokenizer, _ = clone_chat_template( modified_model, modified_tokenizer, source, resize_to_multiple_of=124 ) # Check that the input embeddings have been resized to a multiple of 123 assert (modified_model.vocab_size % 124) == 0 # Check that the input embeddings size matches the tokenizer vocabulary size assert model.vocab_size == len(modified_tokenizer.vocab) def test_apply_new_chat_template(self): # This tokenizer doesn't have a chat_template by default tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-BloomForCausalLM") # This one has a chat_template by default source = "trl-internal-testing/tiny-Qwen3ForCausalLM" _, modified_tokenizer, _ = clone_chat_template(model, tokenizer, source) messages = [ {"role": "system", "content": "You are helpful"}, {"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi, how can I help you?"}, ] prompt = modified_tokenizer.apply_chat_template(messages, tokenize=False) assert ( prompt == "<|im_start|>system\nYou are helpful<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n\n\n\n\nHi, how can I help you?<|im_end|>\n" ) def test_clone_with_sequence_classification_model(self): # This tokenizer doesn't have a chat_template by default tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptNeoXForSequenceClassification") model = AutoModelForSequenceClassification.from_pretrained( "trl-internal-testing/tiny-GptNeoXForSequenceClassification" ) # This one has a chat_template by default source = "trl-internal-testing/tiny-Qwen3ForCausalLM" _, modified_tokenizer, _ = clone_chat_template(model, tokenizer, source) # Check if special tokens are correctly set assert modified_tokenizer.eos_token == "<|im_end|>" @pytest.mark.parametrize( "tokenizer_name", [ pytest.param("trl-internal-testing/tiny-Qwen3MoeForSequenceClassification", id="qwen3"), pytest.param("trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", id="qwen35"), ], ) @pytest.mark.xfail( condition=Version(transformers.__version__) < Version("5.0.0"), reason="Response parsing is not supported in transformers versions below 5.0.0", strict=True, ) @require_jmespath class TestAddResponseSchema: def test_add_response_schema(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) messages = [ {"role": "user", "content": "What is 3*4?"}, { "role": "assistant", "content": "", "tool_calls": [{"type": "function", "function": {"name": "multiply", "arguments": {"a": 3, "b": 4}}}], }, ] prefix = tokenizer.apply_chat_template(messages[:1], tokenize=False, add_generation_prompt=True) text = tokenizer.apply_chat_template(messages, tokenize=False) response = text[len(prefix) :] # Here, we just test that the parsing doesn't raise an error. # The correctness of the parsing is tested in TestParseResponse tokenizer.parse_response(response) class TestIsChatTemplatePrefixPreserving: def test_prefix_preserving_template(self): tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen3MoeForSequenceClassification") tokenizer.chat_template = textwrap.dedent(r""" {%- for message in messages %} {%- if message.role == 'user' %} {{- '<|im_start|>user\n' + message.content + '<|im_end|>\n' }} {%- elif message.role == 'assistant' %} {{- '<|im_start|>assistant\n' + message.content + '<|im_end|>\n' }} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- endif %}""") assert is_chat_template_prefix_preserving(tokenizer) is True def test_non_prefix_preserving_template(self): tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen3MoeForSequenceClassification") # The following template is quite typical of models like Qwen3 and GPT-OSS, where the thinking part is # only present for last assistant message, which makes it non-prefix-preserving. # docstyle-ignore tokenizer.chat_template = textwrap.dedent(r""" {%- if messages[0].role == 'system' %} {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} {%- endif %} {%- set ns = namespace(last_query_index=messages|length - 1) %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if message.role == "user" and message.content is string %} {%- set ns.last_query_index = index %} {%- break %} {%- endif %} {%- endfor %} {%- for message in messages %} {%- set content = message.content if message.content is string else '' %} {%- if message.role == "user" or (message.role == "system" and not loop.first) %} {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>\n' }} {%- elif message.role == "assistant" %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is string %} {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {%- if loop.index0 > ns.last_query_index %} {%- if loop.last or (not loop.last and reasoning_content) %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {{- '<|im_end|>\n' }} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- if enable_thinking is defined and enable_thinking is false %} {{- '\n\n\n\n' }} {%- endif %} {%- endif %}""") assert is_chat_template_prefix_preserving(tokenizer) is False @pytest.mark.parametrize( "tokenizer_name", [ pytest.param("trl-internal-testing/tiny-Qwen3MoeForSequenceClassification", id="qwen3"), pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", id="qwen35", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.0.0"), reason="Qwen3.5 tokenizer requires transformers>=5.0.0", ), ), ], ) class TestGetTrainingChatTemplate: def test_new_chat_template_is_prefix_preserving(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) assert is_chat_template_prefix_preserving(tokenizer) is False tokenizer.chat_template = get_training_chat_template(tokenizer) assert is_chat_template_prefix_preserving(tokenizer) is True def test_behavior_unchanged_single_user_no_generation_prompt(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [{"role": "user", "content": "What color is the sky?"}] before = tokenizer.apply_chat_template(messages, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_single_user_with_generation_prompt(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [{"role": "user", "content": "What color is the sky?"}] before = tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, chat_template=new_chat_template, ) assert before == after def test_behavior_unchanged_single_user_and_final_assistant_plain_content(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] before = tokenizer.apply_chat_template(messages, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_final_assistant_with_reasoning_content(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [ {"role": "user", "content": "What color is the sky?"}, { "role": "assistant", "content": "It is blue.", "reasoning_content": "The sky appears blue due to Rayleigh scattering.", }, ] before = tokenizer.apply_chat_template(messages, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_final_assistant_with_existing_think_tags(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [ {"role": "user", "content": "What color is the sky?"}, { "role": "assistant", "content": "\nThe sky scatters shorter wavelengths.\n\n\nIt is blue.", }, ] before = tokenizer.apply_chat_template(messages, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_assistant_with_tool_calls(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [ {"role": "user", "content": "Multiply 3 by 4."}, { "role": "assistant", "content": "I will call a tool.", "tool_calls": [{"name": "multiply", "arguments": {"a": 3, "b": 4}}], }, ] before = tokenizer.apply_chat_template(messages, tokenize=False) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_with_tools_with_and_without_system_message(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tools = [ { "type": "function", "function": { "name": "multiply", "description": "Multiply two numbers.", "parameters": { "type": "object", "properties": { "a": {"type": "number"}, "b": {"type": "number"}, }, "required": ["a", "b"], }, }, } ] messages = [{"role": "user", "content": "Multiply 3 by 4."}] before = tokenizer.apply_chat_template(messages, tokenize=False, tools=tools) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, tools=tools, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_with_tools_with_system_message(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tools = [ { "type": "function", "function": { "name": "multiply", "description": "Multiply two numbers.", "parameters": { "type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"], }, }, } ] messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Multiply 3 by 4."}, ] before = tokenizer.apply_chat_template(messages, tokenize=False, tools=tools) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template(messages, tokenize=False, tools=tools, chat_template=new_chat_template) assert before == after def test_behavior_unchanged_generation_prompt_with_enable_thinking_false(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) messages = [{"role": "user", "content": "What color is the sky?"}] before = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False ) new_chat_template = get_training_chat_template(tokenizer) after = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False, chat_template=new_chat_template, ) assert before == after @pytest.mark.parametrize( "tokenizer_name", [ pytest.param("trl-internal-testing/tiny-Qwen3MoeForSequenceClassification", id="qwen3"), pytest.param("trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", id="qwen35"), ], ) @pytest.mark.xfail( condition=Version(transformers.__version__) < Version("5.0.0"), reason="Response parsing is not supported in transformers versions below 5.0.0", strict=True, ) @require_jmespath class TestParseResponse: def test_parse_response(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) messages = [ {"role": "user", "content": "What is 3*4?"}, {"role": "assistant", "content": "12"}, ] prefix = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == messages[-1] def test_parse_response_with_reasoning_content(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) messages = [ {"role": "user", "content": "What is 3*4?"}, {"role": "assistant", "reasoning_content": "Hmmm.", "content": "12"}, ] # enable_thinking=True is required here because for Qwen3.5, the thinking is disabled by default for the # generation prompt. prefix = tokenizer.apply_chat_template( messages[:1], add_generation_prompt=True, enable_thinking=True ).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == messages[-1] def test_parse_response_tool_call(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) tool_calls = [{"type": "function", "function": {"name": "multiply", "arguments": {"a": 3, "b": 4}}}] messages = [ {"role": "user", "content": "What is 3*4?"}, {"role": "assistant", "content": "", "tool_calls": tool_calls}, ] prefix = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == messages[-1] def test_parse_response_tool_call_with_content(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) tool_calls = [{"type": "function", "function": {"name": "multiply", "arguments": {"a": 3, "b": 4}}}] messages = [ {"role": "user", "content": "What is 3*4?"}, {"role": "assistant", "content": "Let's call the tool.", "tool_calls": tool_calls}, ] prefix = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == messages[-1] def test_parse_response_tool_call_without_arguments(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) tool_calls = [{"type": "function", "function": {"name": "ping", "arguments": {}}}] messages = [ {"role": "user", "content": "Ping the service."}, {"role": "assistant", "tool_calls": tool_calls}, ] prefix = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == {"role": "assistant", "content": "", "tool_calls": tool_calls} def test_parse_response_multiple_tool_calls(self, tokenizer_name): tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) tool_calls = [ {"type": "function", "function": {"name": "multiply", "arguments": {"a": 3, "b": 4}}}, {"type": "function", "function": {"name": "addition", "arguments": {"a": 4, "b": 3}}}, ] messages = [ {"role": "user", "content": "What is 3*4?"}, {"role": "assistant", "content": "", "tool_calls": tool_calls}, ] prefix = tokenizer.apply_chat_template(messages[:1], add_generation_prompt=True).input_ids text = tokenizer.apply_chat_template(messages).input_ids response = text[len(prefix) :] parsed = parse_response(tokenizer, response) assert parsed == messages[-1] def test_parse_response_malformed_tool_call(self, tokenizer_name): if tokenizer_name != "trl-internal-testing/tiny-Qwen3MoeForSequenceClassification": pytest.skip("For simplicity, we only test the malformed tool call case on one tokenizer.") tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) tokenizer = add_response_schema(tokenizer) text = '\n{"name": "multiply", "arguments": {"a": 3, "b": 4}\n<|im_end|>' assistant_text = tokenizer(text)["input_ids"] parsed = parse_response(tokenizer, assistant_text) expected = { "role": "assistant", "content": '\n{"name": "multiply", "arguments": {"a": 3, "b": 4}\n', } assert parsed == expected ================================================ FILE: tests/test_cli.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from io import StringIO from unittest.mock import patch import pytest import yaml from .testing_utils import TrlTestCase @pytest.mark.parametrize("command", ["dpo", "grpo", "kto", "reward", "rloo", "sft"]) def test_help_no_type_error(command): # Regression test for https://github.com/huggingface/trl/issues/5099: # TrainingArguments help strings with unescaped "%" caused TypeError in argparse. from trl.cli import main with pytest.raises(SystemExit) as exc_info: with patch("sys.argv", ["trl", command, "--help"]), patch("sys.stdout", new_callable=StringIO): main() assert exc_info.value.code == 0 class TestCLI(TrlTestCase): def test_dpo(self): from trl.cli import main command = f"trl dpo --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_preference --report_to none" with patch("sys.argv", command.split(" ")): main() def test_dpo_multiple_loss_types(self): from trl.cli import main command = f"trl dpo --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_preference --report_to none --loss_type sigmoid bco_pair --loss_weights 1.0 0.5" with patch("sys.argv", command.split(" ")): main() @patch("sys.stdout", new_callable=StringIO) def test_env(self, mock_stdout): from trl.cli import main command = "trl env" with patch("sys.argv", command.split(" ")): main() assert "TRL version: " in mock_stdout.getvalue().strip() def test_grpo(self): from trl.cli import main command = f"trl grpo --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --reward_model_name_or_path trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_prompt_only --num_generations 4 --max_completion_length 32 --report_to none" with patch("sys.argv", command.split(" ")): main() def test_kto(self): from trl.cli import main command = f"trl kto --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_unpaired_preference --report_to none" with patch("sys.argv", command.split(" ")): main() def test_reward(self): from trl.cli import main command = f"trl reward --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_implicit_prompt_preference --report_to none" with patch("sys.argv", command.split(" ")): main() def test_rloo(self): from trl.cli import main command = f"trl rloo --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --reward_model_name_or_path trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_prompt_only --num_generations 2 --max_completion_length 32 --report_to none" with patch("sys.argv", command.split(" ")): main() def test_sft(self): from trl.cli import main command = f"trl sft --output_dir {self.tmp_dir} --model_name_or_path trl-internal-testing/tiny-Qwen2ForCausalLM-2.5 --dataset_name trl-internal-testing/zen --dataset_config standard_language_modeling --report_to none" with patch("sys.argv", command.split(" ")): main() def test_sft_config_file(self): from trl.cli import main output_dir = os.path.join(self.tmp_dir, "output") # Create a temporary config file config_path = os.path.join(self.tmp_dir, "config.yaml") config_content = { "model_name_or_path": "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "dataset_name": "trl-internal-testing/zen", "dataset_config": "standard_language_modeling", "report_to": "none", "output_dir": output_dir, "lr_scheduler_type": "cosine_with_restarts", } with open(config_path, "w") as config_file: yaml.dump(config_content, config_file) # Test the CLI with config file command = f"trl sft --config {config_path}" with patch("sys.argv", command.split(" ")): main() # Verify that output directory was created assert os.path.exists(output_dir) def test_vllm_serve_config_file(self): """ Test `trl vllm-serve --config config.yaml` must not raise "the following arguments are required: --model" when the required field is satisfied by the config file rather than the command line. """ from trl.cli import main config_path = os.path.join(self.tmp_dir, "config.yaml") with open(config_path, "w") as f: yaml.dump({"model": "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5"}, f) # Patch the actual function that `VllmServeCommand.run` imports as `vllm_serve_main` with patch("trl.scripts.vllm_serve.main") as mock_serve: with patch("sys.argv", ["trl", "vllm-serve", "--config", config_path]): main() mock_serve.assert_called_once() script_args = mock_serve.call_args.args[0] assert script_args.model == "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" ================================================ FILE: tests/test_cli_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import tempfile from dataclasses import dataclass from unittest.mock import mock_open, patch import pytest from datasets import DatasetDict, load_dataset from trl import DatasetMixtureConfig, TrlParser, get_dataset from trl.scripts.utils import DatasetConfig from .testing_utils import TrlTestCase @dataclass class MyDataclass: arg1: int arg2: str = "default" @dataclass class InvalidDataclass: config: str # This should raise an error in the TrlParser class TestTrlParser(TrlTestCase): def test_init_without_config_field(self): """Test initialization without 'config' field in the dataclasses.""" parser = TrlParser(dataclass_types=[MyDataclass]) assert isinstance(parser, TrlParser) def test_init_with_config_field(self): """Test initialization with a 'config' field in the dataclass (should raise ValueError).""" with pytest.raises(ValueError, match="has a field named 'config'"): TrlParser(dataclass_types=[InvalidDataclass]) @patch("builtins.open", mock_open(read_data="env:\n VAR1: value1\n VAR2: value2\narg1: 2")) @patch("yaml.safe_load") @patch("os.environ", new_callable=dict) # Mock os.environ as a dictionary def test_parse_args_and_config_with_valid_config(self, mock_environ, mock_yaml_load): """Test parse_args_and_config method with valid arguments and config.""" mock_yaml_load.return_value = {"env": {"VAR1": "value1", "VAR2": "value2"}, "arg1": 2} parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg2", "value", "--config", "config.yaml"] # don't set arg1 to test default value # Simulate the config being loaded and environment variables being set result_args = parser.parse_args_and_config(args) # Set the environment variables using the mock mock_environ["VAR1"] = "value1" mock_environ["VAR2"] = "value2" # Ensure that the environment variables were set correctly assert mock_environ.get("VAR1") == "value1" assert mock_environ.get("VAR2") == "value2" # Check the parsed arguments assert len(result_args) == 1 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 2 assert result_args[0].arg2 == "value" @patch("builtins.open", mock_open(read_data="arg1: 2")) @patch("yaml.safe_load") def test_parse_args_and_arg_override_config(self, mock_yaml_load): """Test parse_args_and_config method and check that arguments override the config.""" mock_yaml_load.return_value = {"arg1": 2} # this arg is meant to be overridden parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg1", "3", "--config", "config.yaml"] # override arg1 default with 3 # Simulate the config being loaded and arguments being passed result_args = parser.parse_args_and_config(args) # Check the parsed arguments assert len(result_args) == 1 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 3 @patch("builtins.open", mock_open(read_data="env: not_a_dict")) @patch("yaml.safe_load") def test_parse_args_and_config_with_invalid_env(self, mock_yaml_load): """Test parse_args_and_config method when the 'env' field is not a dictionary.""" mock_yaml_load.return_value = {"env": "not_a_dict"} parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg1", "2", "--arg2", "value", "--config", "config.yaml"] with pytest.raises(ValueError, match="`env` field should be a dict in the YAML file."): parser.parse_args_and_config(args) def test_parse_args_and_config_without_config(self): """Test parse_args_and_config without the `--config` argument.""" parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg1", "2", "--arg2", "value"] # Simulate no config, just parse args normally result_args = parser.parse_args_and_config(args) # Check that the arguments are parsed as is assert len(result_args) == 1 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 2 assert result_args[0].arg2 == "value" def test_set_defaults_with_config(self): """Test set_defaults_with_config updates the defaults.""" parser = TrlParser(dataclass_types=[MyDataclass]) # Update defaults parser.set_defaults_with_config(arg1=42) # Ensure the default value is updated result_args = parser.parse_args_and_config([]) assert len(result_args) == 1 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 42 def test_parse_args_and_config_with_remaining_strings(self): parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg1", "2", "--arg2", "value", "remaining"] # Simulate no config, just parse args normally result_args = parser.parse_args_and_config(args, return_remaining_strings=True) # Check that the arguments are parsed as is assert len(result_args) == 2 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 2 assert result_args[0].arg2 == "value" assert result_args[1] == ["remaining"] @patch("builtins.open", mock_open(read_data="remaining_string_in_config: abc")) @patch("yaml.safe_load") def test_parse_args_and_config_with_remaining_strings_in_config_and_args(self, mock_yaml_load): mock_yaml_load.return_value = {"remaining_string_in_config": "abc"} parser = TrlParser(dataclass_types=[MyDataclass]) args = ["--arg1", "2", "--remaining_string_in_args", "def", "--config", "config.yaml"] # Simulate the config being loaded and arguments being passed result_args = parser.parse_args_and_config(args, return_remaining_strings=True) # Check that the arguments are parsed as is assert len(result_args) == 2 assert isinstance(result_args[0], MyDataclass) assert result_args[0].arg1 == 2 assert result_args[1] == ["--remaining_string_in_config", "abc", "--remaining_string_in_args", "def"] @patch("builtins.open", mock_open(read_data="arg1: 2\narg2: config_value")) @patch("yaml.safe_load") def test_subparsers_with_config_defaults(self, mock_yaml_load): """Test that config defaults are applied to all subparsers.""" mock_yaml_load.return_value = {"arg1": 2, "arg2": "config_value"} # Create the main parser parser = TrlParser() # Add subparsers subparsers = parser.add_subparsers(dest="command", parser_class=TrlParser) # Create a subparser for a specific command subparsers.add_parser("subcommand", dataclass_types=[MyDataclass]) # Parse with config file args = ["subcommand", "--config", "config.yaml"] result_args = parser.parse_args_and_config(args) # Check main parser arguments assert len(result_args) == 1 # Check that config values were applied to the subparser assert result_args[0].arg1 == 2 # Default from config assert result_args[0].arg2 == "config_value" # Default from config @patch("builtins.open", mock_open(read_data="arg1: 2\narg2: config_value")) @patch("yaml.safe_load") def test_subparsers_with_config_defaults_and_arg_override(self, mock_yaml_load): """Test that config defaults are applied to all subparsers.""" mock_yaml_load.return_value = {"arg1": 2, "arg2": "config_value"} # Create the main parser parser = TrlParser() # Add subparsers subparsers = parser.add_subparsers(dest="command", parser_class=TrlParser) # Create a subparser for a specific command subparsers.add_parser("subcommand", dataclass_types=[MyDataclass]) # Test with command line arguments overriding config args = ["subcommand", "--arg1", "3", "--config", "config.yaml"] result_args = parser.parse_args_and_config(args) # Command line arguments should override config assert result_args[0].arg1 == 3 assert result_args[0].arg2 == "config_value" # Still from config @patch("builtins.open", mock_open(read_data="arg1: 2\nthis_arg_does_not_exist: config_value")) @patch("yaml.safe_load") def test_subparsers_with_config_defaults_and_arg_override_wrong_name(self, mock_yaml_load): """Test that config defaults are applied to all subparsers.""" mock_yaml_load.return_value = {"arg1": 2, "this_arg_does_not_exist": "config_value"} # Create the main parser parser = TrlParser() # Add subparsers subparsers = parser.add_subparsers(dest="command", parser_class=TrlParser) # Create a subparser for a specific command subparsers.add_parser("subcommand", dataclass_types=[MyDataclass]) # Test with command line arguments overriding config args = ["subcommand", "--arg1", "3", "--config", "config.yaml"] with pytest.raises(ValueError): parser.parse_args_and_config(args) parser.parse_args_and_config(args, fail_with_unknown_args=False) @patch("builtins.open", mock_open(read_data="arg1: 2\narg2: config_value")) @patch("yaml.safe_load") def test_subparsers_multiple_with_config_defaults(self, mock_yaml_load): """Test that config defaults are applied to all subparsers.""" mock_yaml_load.return_value = {"arg1": 2, "arg2": "config_value"} # Create the main parser parser = TrlParser() # Add subparsers subparsers = parser.add_subparsers(dest="command", parser_class=TrlParser) # Create a subparser for a specific command subparsers.add_parser("subcommand0", dataclass_types=[MyDataclass]) subparsers.add_parser("subcommand1", dataclass_types=[MyDataclass]) for idx in range(2): # Parse with config file args = [f"subcommand{idx}", "--config", "config.yaml"] result_args = parser.parse_args_and_config(args) # Check main parser arguments assert len(result_args) == 1 # Check that config values were applied to the subparser assert result_args[0].arg1 == 2 # Default from config assert result_args[0].arg2 == "config_value" # Default from config class TestGetDataset: def test_single_dataset_with_config(self): mixture_config = DatasetMixtureConfig( datasets=[DatasetConfig(path="trl-internal-testing/zen", name="standard_language_modeling")] ) result = get_dataset(mixture_config) expected = load_dataset("trl-internal-testing/zen", "standard_language_modeling") assert expected["train"][:] == result["train"][:] def test_single_dataset_preference_config(self): mixture_config = DatasetMixtureConfig( datasets=[DatasetConfig(path="trl-internal-testing/zen", name="standard_preference")] ) result = get_dataset(mixture_config) expected = load_dataset("trl-internal-testing/zen", "standard_preference") assert expected["train"][:] == result["train"][:] def test_single_dataset_streaming(self): mixture_config = DatasetMixtureConfig( datasets=[DatasetConfig(path="trl-internal-testing/zen", name="standard_language_modeling")], streaming=True, ) result = get_dataset(mixture_config) expected = load_dataset("trl-internal-testing/zen", "standard_language_modeling") assert expected["train"].to_list() == list(result["train"]) def test_dataset_mixture_basic(self): dataset_config1 = DatasetConfig( path="trl-internal-testing/zen", name="standard_prompt_completion", split="train", columns=["prompt"] ) dataset_config2 = DatasetConfig( path="trl-internal-testing/zen", name="standard_preference", split="train", columns=["prompt"] ) mixture_config = DatasetMixtureConfig(datasets=[dataset_config1, dataset_config2]) result = get_dataset(mixture_config) assert isinstance(result, DatasetDict) assert "train" in result train_dataset = result["train"] assert train_dataset.column_names == ["prompt"] prompts = train_dataset["prompt"] expected_first_half = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") assert prompts[: len(prompts) // 2] == expected_first_half["prompt"] expected_second_half = load_dataset("trl-internal-testing/zen", "standard_prompt_completion", split="train") assert prompts[len(prompts) // 2 :] == expected_second_half["prompt"] def test_dataset_mixture_with_weights(self): dataset_config1 = DatasetConfig( path="trl-internal-testing/zen", name="standard_prompt_completion", split="train[:50%]", columns=["prompt"] ) dataset_config2 = DatasetConfig( path="trl-internal-testing/zen", name="standard_preference", split="train[:50%]", columns=["prompt"] ) mixture_config = DatasetMixtureConfig(datasets=[dataset_config1, dataset_config2]) result = get_dataset(mixture_config) assert isinstance(result, DatasetDict) assert "train" in result train_dataset = result["train"] assert train_dataset.column_names == ["prompt"] prompts = train_dataset["prompt"] expected_first_half = load_dataset("trl-internal-testing/zen", "standard_preference", split="train[:50%]") assert prompts[: len(prompts) // 2] == expected_first_half["prompt"] expected_second_half = load_dataset( "trl-internal-testing/zen", "standard_prompt_completion", split="train[:50%]" ) assert prompts[len(prompts) // 2 :] == expected_second_half["prompt"] def test_dataset_mixture_with_test_split(self): mixture_config = DatasetMixtureConfig( datasets=[DatasetConfig(path="trl-internal-testing/zen", name="standard_language_modeling")], test_split_size=2, ) result = get_dataset(mixture_config) assert isinstance(result, DatasetDict) assert "train" in result assert "test" in result assert len(result["train"]) == 15 assert len(result["test"]) == 2 def test_empty_dataset_mixture_raises_error(self): mixture_config = DatasetMixtureConfig(datasets=[]) with pytest.raises(ValueError, match="No datasets were loaded"): get_dataset(mixture_config) def test_mixture_multiple_different_configs(self): dataset_config1 = DatasetConfig( path="trl-internal-testing/zen", name="conversational_preference", split="train", columns=["prompt"] ) dataset_config2 = DatasetConfig( path="trl-internal-testing/zen", name="conversational_prompt_only", split="test" ) mixture_config = DatasetMixtureConfig(datasets=[dataset_config1, dataset_config2]) result = get_dataset(mixture_config) assert isinstance(result, DatasetDict) assert "train" in result assert len(result["train"]) > 0 def test_trlparser_parses_yaml_config_correctly(self): # Prepare YAML content exactly like your example # docstyle-ignore yaml_content = """ datasets: - path: trl-internal-testing/zen name: standard_prompt_only - path: trl-internal-testing/zen name: standard_preference columns: - prompt """ # Write YAML to a temporary file with tempfile.NamedTemporaryFile("w+", suffix=".yaml") as tmpfile: tmpfile.write(yaml_content) tmpfile.flush() parser = TrlParser((DatasetMixtureConfig,)) args = parser.parse_args_and_config(args=["--config", tmpfile.name])[0] # Assert that we got DatasetMixtureConfig instance assert isinstance(args, DatasetMixtureConfig) # Assert datasets list length assert len(args.datasets) == 2 # Check first dataset dataset_config1 = args.datasets[0] assert isinstance(dataset_config1, DatasetConfig) assert dataset_config1.path == "trl-internal-testing/zen" assert dataset_config1.name == "standard_prompt_only" assert dataset_config1.columns is None # No columns specified # Check second dataset dataset_config2 = args.datasets[1] assert isinstance(dataset_config2, DatasetConfig) assert dataset_config2.path == "trl-internal-testing/zen" assert dataset_config2.name == "standard_preference" assert dataset_config2.columns == ["prompt"] # Columns specified def test_trlparser_parses_yaml_and_loads_dataset(self): # Prepare YAML content exactly like your example # docstyle-ignore yaml_content = """ datasets: - path: trl-internal-testing/zen name: standard_language_modeling """ # Write YAML to a temporary file with tempfile.NamedTemporaryFile("w+", suffix=".yaml") as tmpfile: tmpfile.write(yaml_content) tmpfile.flush() parser = TrlParser((DatasetMixtureConfig,)) args = parser.parse_args_and_config(args=["--config", tmpfile.name])[0] # Load the dataset using get_dataset result = get_dataset(args) expected = load_dataset("trl-internal-testing/zen", "standard_language_modeling") assert expected["train"][:] == result["train"][:] ================================================ FILE: tests/test_data_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import textwrap from time import strftime import pytest import transformers from datasets import Dataset, DatasetDict from packaging.version import Version from transformers import AutoProcessor, AutoTokenizer, is_vision_available from trl.data_utils import ( apply_chat_template, extract_prompt, is_conversational, is_conversational_from_value, maybe_apply_chat_template, maybe_convert_to_chatml, maybe_extract_prompt, maybe_unpair_preference_dataset, pack_dataset, prepare_multimodal_messages, prepare_multimodal_messages_vllm, truncate_dataset, unpair_preference_dataset, ) from .testing_utils import TrlTestCase, require_vision if is_vision_available(): from PIL import Image @require_vision class TestPrepareMultimodalMessages: def test_basic_user_assistant_conversation(self): """Test basic conversation with user and assistant messages.""" messages = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] image = Image.new("RGB", (10, 10), color="blue") messages = prepare_multimodal_messages(messages, images=[image]) expected = [ { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "What color is the sky?"}], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, ] assert messages == expected def test_first_user_message_gets_image(self): """Test that only the first user message gets an image.""" messages = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, {"role": "user", "content": "How about the grass?"}, ] image = Image.new("RGB", (10, 10), color="blue") messages = prepare_multimodal_messages(messages, images=[image]) expected = [ { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "What color is the sky?"}], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, { "role": "user", "content": [{"type": "text", "text": "How about the grass?"}], }, ] assert messages == expected def test_multiple_images(self): """Test that multiple images are added to the first user message.""" messages = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] images = [Image.new("RGB", (10, 10), color=color) for color in ["red", "green", "blue"]] messages = prepare_multimodal_messages(messages, images=images) expected = [ { "role": "user", "content": [ {"type": "image", "image": images[0]}, {"type": "image", "image": images[1]}, {"type": "image", "image": images[2]}, {"type": "text", "text": "What color is the sky?"}, ], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, ] assert messages == expected def test_system_message_transformation(self): """Test that system messages are properly transformed.""" messages = [ {"role": "system", "content": "You are a helpful assistant"}, {"role": "user", "content": "What color is the sky?"}, ] image = Image.new("RGB", (10, 10), color="blue") messages = prepare_multimodal_messages(messages, images=[image]) expected = [ { "role": "system", "content": [{"type": "text", "text": "You are a helpful assistant"}], }, { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "What color is the sky?"}], }, ] assert messages == expected def test_already_prepared_messages_unchanged(self): """Test that messages with list content are not modified.""" messages = [ {"role": "system", "content": [{"type": "text", "text": "You are a helpful assistant"}]}, {"role": "user", "content": [{"type": "image"}, {"type": "text", "text": "What color is the sky?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "It is blue."}]}, ] image = Image.new("RGB", (10, 10), color="blue") messages = prepare_multimodal_messages(messages, images=[image]) expected = [ { "role": "system", "content": [{"type": "text", "text": "You are a helpful assistant"}], }, { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "What color is the sky?"}], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, ] assert messages == expected def test_mixed_prepared_and_unprepared_messages(self): """Test handling of mixed prepared and unprepared messages.""" messages = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": [{"type": "text", "text": "It is blue."}]}, {"role": "user", "content": "What about the grass?"}, ] image = Image.new("RGB", (10, 10), color="blue") messages = prepare_multimodal_messages(messages, images=[image]) expected = [ { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "What color is the sky?"}], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, { "role": "user", "content": [{"type": "text", "text": "What about the grass?"}], }, ] assert messages == expected def test_message_with_tool_calling_turns(self): """Test that both the assistant tool call and the tool role turns messages are properly transformed.""" messages = [ {"role": "user", "content": "What's the weather like in New York?"}, { "role": "assistant", "tool_calls": [ { "type": "tool", "function": {"name": "get_current_weather", "arguments": {"location": "New York"}}, } ], }, {"role": "tool", "name": "get_current_weather", "content": "22.0"}, {"role": "assistant", "content": "The current weather in New York is 22.0 degrees Celsius."}, ] messages = prepare_multimodal_messages(messages, images=[]) expected = [ { "role": "user", "content": [{"type": "text", "text": "What's the weather like in New York?"}], }, { "role": "assistant", "tool_calls": [ { "type": "tool", "function": {"name": "get_current_weather", "arguments": {"location": "New York"}}, } ], }, {"role": "tool", "name": "get_current_weather", "content": "22.0"}, { "role": "assistant", "content": [{"type": "text", "text": "The current weather in New York is 22.0 degrees Celsius."}], }, ] assert messages == expected @require_vision class TestPrepareMultimodalMessagesVLLM: def test_single_image_conversion(self): messages = [ { "role": "user", "content": [ {"type": "image", "image": Image.new("RGB", (10, 10), color="blue")}, {"type": "text", "text": "What color is the sky?"}, ], } ] result = prepare_multimodal_messages_vllm(messages) # Original should remain unchanged (deepcopy test) assert messages[0]["content"][0]["type"] == "image" # Converted version should have correct structure assert result[0]["content"][0]["type"] == "image_pil" assert "image_pil" in result[0]["content"][0] assert "image" not in result[0]["content"][0] assert isinstance(result[0]["content"][0]["image_pil"], Image.Image) assert result[0]["content"][1]["type"] == "text" def test_mixed_content_conversion(self): messages = [ { "role": "user", "content": [ {"type": "text", "text": "What color is the sky?"}, {"type": "image", "image": Image.new("RGB", (10, 10), color="blue")}, ], } ] result = prepare_multimodal_messages_vllm(messages) # The image part should be converted, text should be unchanged assert result[0]["content"][0]["type"] == "text" assert result[0]["content"][1]["type"] == "image_pil" def test_no_images(self): messages = [{"role": "user", "content": [{"type": "text", "text": "What color is the sky?"}]}] result = prepare_multimodal_messages_vllm(messages) # Should be identical since there are no images assert result == messages # And a deepcopy — not the same object assert result is not messages assert result[0] is not messages[0] def test_multiple_messages(self): messages = [ { "role": "user", "content": [ {"type": "text", "text": "What color is the sky?"}, {"type": "image", "image": Image.new("RGB", (10, 10), color="blue")}, ], }, { "role": "assistant", "content": [{"type": "text", "text": "It is blue."}], }, ] result = prepare_multimodal_messages_vllm(messages) assert result[0]["content"][1]["type"] == "image_pil" assert result[1]["content"][0]["type"] == "text" assert result[1]["content"][0]["text"] == "It is blue." def test_deepcopy_integrity(self): messages = [ { "role": "user", "content": [ {"type": "text", "text": "What color is the sky?"}, {"type": "image", "image": Image.new("RGB", (10, 10), color="blue")}, ], }, ] original = copy.deepcopy(messages) _ = prepare_multimodal_messages_vllm(messages) # Original should not be mutated assert messages == original class TestIsConversational(TrlTestCase): # fmt: off conversational_examples = [ { # Language modeling "messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], }, { # Prompt-only "prompt": [{"role": "user", "content": "What color is the sky?"}], }, { # Prompt-completion "prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], }, { # Preference "prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}], }, { # Preference with implicit prompt "chosen": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], "rejected": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}, ], }, { # Preference with tool calls "prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_color", "arguments": {"what": "sky"}}}]}, {"role": "tool", "name": "get_color", "content": "blue"}, {"role": "assistant", "content": "It is blue."}, ], "rejected": [ {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "get_color", "arguments": {"what": "tree"}}}]}, {"role": "tool", "name": "get_color", "content": "green"}, {"role": "assistant", "content": "It is green."}, ], "tools": [ { "type": "function", "function": { "description": "Gets the color.", "name": "get_color", "parameters": {"properties": {"what": {"description": "What to get the color of.", "type": "string"}}, "required": ["what"], "type": "object"}, "return": {"description": "The color.", "type": "string"}, }, }, ], }, { # Unpaired preference "prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], "label": True, }, { # Language modeling with harmony "messages": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], }, { # Prompt-only with harmony "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], }, { # Prompt-completion with harmony "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "completion": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], }, { # Preference with harmony "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "chosen": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "rejected": [ {"role": "assistant", "thinking": "The user asks the color of the tree...", "content": "It is green."}, ], }, { # Preference with implicit prompt and harmony "chosen": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "rejected": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the tree...", "content": "It is green."}, ], }, { # Unpaired preference with harmony "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "completion": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "label": True, }, ] # fmt: on non_conversational_examples = [ {"prompt": "The sky is", "completion": " blue."}, {"text": "The sky is blue."}, {"prompt": "The sky is"}, {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."}, {"prompt": "The sky is", "completion": " blue.", "label": True}, ] @pytest.mark.parametrize("example", conversational_examples) def test_conversational(self, example): assert is_conversational(example) @pytest.mark.parametrize("example", non_conversational_examples) def test_non_conversational(self, example): assert not is_conversational(example) class TestIsConversationalFromValue(TrlTestCase): def test_positive_1(self): example = { "conversations": [ {"from": "user", "value": "What color is the sky?"}, {"from": "assistant", "value": "It is blue."}, ], } assert is_conversational_from_value(example) def test_negative_1(self): example = { "messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], } assert not is_conversational_from_value(example) def test_negative_2(self): example = {"text": "The sky is blue."} assert not is_conversational_from_value(example) class TestApplyChatTemplate(TrlTestCase): tokenizers = [ "trl-internal-testing/tiny-CohereForCausalLM", "trl-internal-testing/tiny-Cohere2ForCausalLM", "trl-internal-testing/tiny-DeepseekV3ForCausalLM", "trl-internal-testing/tiny-DeepseekV3ForCausalLM-0528", "trl-internal-testing/tiny-FalconMambaForCausalLM", "trl-internal-testing/tiny-Gemma2ForCausalLM", "trl-internal-testing/tiny-GemmaForCausalLM", "trl-internal-testing/tiny-GptOssForCausalLM", pytest.param( "trl-internal-testing/tiny-Glm4MoeForCausalLM", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.0.0"), reason="GLM4 tokenizer requires transformers>=5.0.0", ), ), "trl-internal-testing/tiny-LlamaForCausalLM-3.1", "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-LlamaForCausalLM-3", "trl-internal-testing/tiny-MistralForCausalLM-0.1", "trl-internal-testing/tiny-MistralForCausalLM-0.2", "trl-internal-testing/tiny-Phi3ForCausalLM", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "trl-internal-testing/tiny-Qwen3ForCausalLM", pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.0.0"), reason="Qwen3.5 tokenizer requires transformers>=5.0.0", ), ), ] conversational_examples = [ { # Language modeling "messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], }, { # Prompt-only "prompt": [{"role": "user", "content": "What color is the sky?"}], }, { # Prompt-completion "prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], }, { # Preference "prompt": [{"role": "user", "content": "What color is the sky?"}], "chosen": [{"role": "assistant", "content": "It is blue."}], "rejected": [{"role": "assistant", "content": "It is green."}], }, { # Preference with implicit prompt "chosen": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], "rejected": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}, ], }, { # Unpaired preference "prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], "label": True, }, ] non_conversational_examples = [ {"text": "The sky is blue."}, # Language modeling {"prompt": "The sky is"}, # Prompt-only {"prompt": "The sky is", "completion": " blue."}, # Prompt-completion {"prompt": "The sky is", "chosen": " blue.", "rejected": " green."}, # Preference {"chosen": "The sky is blue.", "rejected": "The sky is green."}, # Preference with implicit prompt {"prompt": "The sky is", "completion": " blue.", "label": True}, # Unpaired preference ] @pytest.mark.parametrize("example", conversational_examples) @pytest.mark.parametrize("tokenizer_id", tokenizers) def test_apply_chat_template(self, tokenizer_id, example): tokenizer = AutoTokenizer.from_pretrained(tokenizer_id) result = apply_chat_template(example, tokenizer) # Checking if the result is a dictionary assert isinstance(result, dict) # The chat template should be applied to the following keys for key in ["prompt", "chosen", "rejected", "completion"]: if key in example: assert key in result assert isinstance(result[key], str) # Exception for messages, the key is "text" once the chat template is applied if "messages" in example: assert "text" in result assert isinstance(result["text"], str) # The label should be kept if "label" in example: assert "label" in result assert isinstance(result["label"], bool) assert result["label"] == example["label"] # both conversational and non-conversational examples @pytest.mark.parametrize("example", conversational_examples + non_conversational_examples) @pytest.mark.parametrize("tokenizer_id", tokenizers) def test_maybe_apply_chat_template(self, tokenizer_id, example): tokenizer = AutoTokenizer.from_pretrained(tokenizer_id) result = maybe_apply_chat_template(example, tokenizer) # Checking if the result is a dictionary assert isinstance(result, dict) # The chat template should be applied to the following keys for key in ["prompt", "chosen", "rejected", "completion"]: if key in example: assert key in result assert isinstance(result[key], str) # Exception for messages, the key is "text" once the chat template is applied if "messages" in example: assert "text" in result assert isinstance(result["text"], str) # The label should be kept if "label" in example: assert "label" in result assert isinstance(result["label"], bool) assert result["label"] == example["label"] def test_apply_chat_template_with_chat_template_kwargs(self): tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen3ForCausalLM") example = { "prompt": [{"role": "user", "content": "What color is the sky?"}], # with this tokenizer, when you pass enable_thinking=False, it will add "\n\n\n\n" "chat_template_kwargs": {"enable_thinking": False}, } result = apply_chat_template(example, tokenizer) # docstyle-ignore expected = textwrap.dedent("""\ <|im_start|>user What color is the sky?<|im_end|> <|im_start|>assistant """) assert result["prompt"] == expected def test_apply_chat_template_with_tools(self): tokenizer = AutoProcessor.from_pretrained("trl-internal-testing/tiny-LlamaForCausalLM-3.2") # Define dummy test tools def get_current_temperature(location: str): """ Gets the temperature at a given location. Args: location: The location to get the temperature for """ return 22.0 # Define test case test_case = { "prompt": [ {"content": "What's the temperature in London?", "role": "user"}, ] } # Test with tools result_with_tools = apply_chat_template(test_case, tokenizer, tools=[get_current_temperature]) # Verify tools are included in the output assert "get_current_temperature" in result_with_tools["prompt"] # Test without tools result_without_tools = apply_chat_template(test_case, tokenizer, tools=None) # Verify tools are not included in the output assert "get_current_temperature" not in result_without_tools["prompt"] class TestApplyChatTemplateHarmony(TrlTestCase): def test_language_modeling(self): messages = { "messages": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant<|channel|>analysis<|message|>The user asks the color of the sky...<|end|><|start|>assistant<|channel|>final<|message|>It is blue.<|return|>""") assert output["text"] == expected def test_prompt_only(self): messages = { "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant""") assert output["prompt"] == expected def test_prompt_completion(self): messages = { "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "completion": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected_prompt = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant""") expected_completion = "<|channel|>analysis<|message|>The user asks the color of the sky...<|end|><|start|>assistant<|channel|>final<|message|>It is blue.<|return|>" assert output["prompt"] == expected_prompt assert output["completion"] == expected_completion def test_preference(self): messages = { "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "chosen": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "rejected": [ {"role": "assistant", "thinking": "The user asks the color of the tree...", "content": "It is green."}, ], } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected_prompt = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant""") expected_chosen = "<|channel|>analysis<|message|>The user asks the color of the sky...<|end|><|start|>assistant<|channel|>final<|message|>It is blue.<|return|>" expected_rejected = "<|channel|>analysis<|message|>The user asks the color of the tree...<|end|><|start|>assistant<|channel|>final<|message|>It is green.<|return|>" assert output["prompt"] == expected_prompt assert output["chosen"] == expected_chosen assert output["rejected"] == expected_rejected def test_preference_with_implicit_prompt(self): messages = { "chosen": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "rejected": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "thinking": "The user asks the color of the tree...", "content": "It is green."}, ], } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected_chosen = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant<|channel|>analysis<|message|>The user asks the color of the sky...<|end|><|start|>assistant<|channel|>final<|message|>It is blue.<|return|>""") # docstyle-ignore expected_rejected = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant<|channel|>analysis<|message|>The user asks the color of the tree...<|end|><|start|>assistant<|channel|>final<|message|>It is green.<|return|>""") assert output["chosen"] == expected_chosen assert output["rejected"] == expected_rejected def test_unpaired_preference(self): messages = { "prompt": [ {"role": "system", "content": "Respond in a friendly manner."}, {"role": "user", "content": "What color is the sky?"}, ], "completion": [ {"role": "assistant", "thinking": "The user asks the color of the sky...", "content": "It is blue."}, ], "label": True, } output = apply_chat_template( messages, tokenizer=AutoTokenizer.from_pretrained("trl-internal-testing/tiny-GptOssForCausalLM"), reasoning_effort="low", model_identity="You are HuggingGPT.", ) # docstyle-ignore expected_prompt = textwrap.dedent(f"""\ <|start|>system<|message|>You are HuggingGPT. Knowledge cutoff: 2024-06 Current date: {strftime("%Y-%m-%d")} Reasoning: low # Valid channels: analysis, commentary, final. Channel must be included for every message.<|end|><|start|>developer<|message|># Instructions Respond in a friendly manner. <|end|><|start|>user<|message|>What color is the sky?<|end|><|start|>assistant""") expected_completion = "<|channel|>analysis<|message|>The user asks the color of the sky...<|end|><|start|>assistant<|channel|>final<|message|>It is blue.<|return|>" assert output["prompt"] == expected_prompt assert output["completion"] == expected_completion assert output["label"] class TestUnpairPreferenceDataset(TrlTestCase): paired_dataset = Dataset.from_dict( { "prompt": ["The sky is", "The sun is"], "chosen": [" blue.", " in the sky."], "rejected": [" green.", " in the sea."], } ) unpaired_dataset = Dataset.from_dict( { "prompt": ["The sky is", "The sun is", "The sky is", "The sun is"], "completion": [" blue.", " in the sky.", " green.", " in the sea."], "label": [True, True, False, False], } ) def test_unpair_preference_dataset(self): # Test that a paired dataset is correctly converted to unpaired unpaired_dataset = unpair_preference_dataset(self.paired_dataset) assert unpaired_dataset.to_dict() == self.unpaired_dataset.to_dict(), ( "The paired dataset should be converted to unpaired." ) def test_unpair_preference_dataset_dict(self): # Test that a paired dataset dict is correctly converted to unpaired paired_dataset_dict = DatasetDict({"abc": self.paired_dataset}) unpaired_dataset_dict = unpair_preference_dataset(paired_dataset_dict) assert unpaired_dataset_dict["abc"].to_dict() == self.unpaired_dataset.to_dict(), ( "The paired dataset should be converted to unpaired." ) def test_maybe_unpair_preference_dataset(self): # Test that a paired dataset is correctly converted to unpaired with maybe_unpair_preference_dataset unpaired_dataset = maybe_unpair_preference_dataset(self.paired_dataset) assert unpaired_dataset.to_dict() == self.unpaired_dataset.to_dict(), ( "The paired dataset should be converted to unpaired." ) def test_maybe_unpair_preference_dataset_dict(self): # Test that a paired dataset dict is correctly converted to unpaired with maybe_unpair_preference_dataset paired_dataset_dict = DatasetDict({"abc": self.paired_dataset}) unpaired_dataset_dict = maybe_unpair_preference_dataset(paired_dataset_dict) assert unpaired_dataset_dict["abc"].to_dict() == self.unpaired_dataset.to_dict(), ( "The paired dataset should be converted to unpaired." ) def test_maybe_unpair_preference_dataset_already_paired(self): # Test that a paired dataset remains unchanged with maybe_unpair_preference_dataset unpaired_dataset = maybe_unpair_preference_dataset(self.unpaired_dataset) assert unpaired_dataset.to_dict() == self.unpaired_dataset.to_dict(), ( "The unpaired dataset should remain unchanged." ) def test_maybe_unpair_preference_dataset_dict_already_paired(self): # Test that a paired dataset dict remains unchanged with maybe_unpair_preference_dataset unpaired_dataset_dict = maybe_unpair_preference_dataset(DatasetDict({"abc": self.unpaired_dataset})) assert unpaired_dataset_dict["abc"].to_dict() == self.unpaired_dataset.to_dict(), ( "The unpaired dataset should remain unchanged." ) class TestExtractPrompt(TrlTestCase): example_implicit_prompt_conversational = { "chosen": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ], "rejected": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}, ], } example_explicit_prompt_conversational = { "prompt": [ {"role": "user", "content": "What color is the sky?"}, ], "chosen": [ {"role": "assistant", "content": "It is blue."}, ], "rejected": [ {"role": "assistant", "content": "It is green."}, ], } example_implicit_prompt_standard = { "chosen": "The sky is blue.", "rejected": "The sky is green.", } example_explicit_prompt_standard = { "prompt": "The sky is", "chosen": " blue.", "rejected": " green.", } def test_extract_prompt_conversational(self): # Test that the prompt is correctly extracted from the dataset example_extracted_prompt = extract_prompt(self.example_implicit_prompt_conversational) assert example_extracted_prompt == self.example_explicit_prompt_conversational, ( "The prompt is not correctly extracted from the dataset." ) def test_maybe_extract_prompt_conversational(self): # Test that the prompt is correctly extracted from the dataset with maybe_extract_prompt example_extracted_prompt = maybe_extract_prompt(self.example_implicit_prompt_conversational) assert example_extracted_prompt == self.example_explicit_prompt_conversational, ( "The prompt is not correctly extracted from the dataset." ) def test_maybe_extract_prompt_conversational_already_explicit(self): # Test that the prompt remains unchanged with maybe_extract_prompt example_extracted_prompt = maybe_extract_prompt(self.example_explicit_prompt_conversational) assert example_extracted_prompt == self.example_explicit_prompt_conversational, ( "The prompt should remain unchanged." ) def test_extract_prompt_standard(self): # Test that the prompt is correctly extracted from the dataset example_extracted_prompt = extract_prompt(self.example_implicit_prompt_standard) assert example_extracted_prompt == self.example_explicit_prompt_standard, ( "The prompt is not correctly extracted from the dataset." ) def test_maybe_extract_prompt_standard(self): # Test that the prompt is correctly extracted from the dataset with maybe_extract_prompt example_extracted_prompt = maybe_extract_prompt(self.example_implicit_prompt_standard) assert example_extracted_prompt == self.example_explicit_prompt_standard, ( "The prompt is not correctly extracted from the dataset." ) def test_maybe_extract_prompt_standard_already_explicit(self): # Test that the prompt remains unchanged with maybe_extract_prompt example_extracted_prompt = maybe_extract_prompt(self.example_explicit_prompt_standard) assert example_extracted_prompt == self.example_explicit_prompt_standard, "The prompt should remain unchanged." class TestPackDatasetWrapped(TrlTestCase): def test_with_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples) dataset = dataset.with_format("numpy", dtype="float32") format = dataset.format seq_length = 3 expected_output = { "input_ids": [[1, 2, 3], [4, 5, 6], [7, 8]], "attention_mask": [[0, 1, 1], [0, 0, 1], [1, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="wrapped") assert dataset.to_dict() == expected_output assert format == dataset.format def test_with_iterable_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples).to_iterable_dataset() dataset = dataset.with_format("numpy") formatting = dataset._formatting seq_length = 3 expected_output = { "input_ids": [[1, 2, 3], [4, 5, 6], [7, 8]], "attention_mask": [[0, 1, 1], [0, 0, 1], [1, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="wrapped") num_examples = len(examples[next(iter(examples))]) assert next(iter(dataset.with_format(None).batch(batch_size=num_examples))) == expected_output assert formatting == dataset._formatting class TestPackDatasetBfd(TrlTestCase): def test_with_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], } dataset = Dataset.from_dict(examples) dataset = dataset.with_format("numpy", dtype="float32") format = dataset.format seq_length = 4 expected_output = { "input_ids": [[4, 5, 6, 7], [1, 2, 3, 8]], "seq_lengths": [[4], [3, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd") expected_format = dataset.format assert dataset.to_dict() == expected_output assert "seq_lengths" in expected_format["columns"] expected_format["columns"].remove("seq_lengths") assert format == dataset.format def test_with_iterable_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], } dataset = Dataset.from_dict(examples).to_iterable_dataset() dataset = dataset.with_format("numpy") formatting = dataset._formatting seq_length = 4 expected_output = { "input_ids": [[4, 5, 6, 7], [1, 2, 3, 8]], "seq_lengths": [[4], [3, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd") num_examples = len(examples[next(iter(examples))]) assert next(iter(dataset.with_format(None).batch(batch_size=num_examples))) == expected_output assert formatting == dataset._formatting def test_with_overlong_0(self): examples = { "input_ids": [[1, 2, 3, 4, 5], [6, 7], [8, 9, 10, 11], [12]], } dataset = Dataset.from_dict(examples) seq_length = 4 expected_output = { "input_ids": [[1, 2, 3, 4], [8, 9, 10, 11], [6, 7, 5, 12]], "seq_lengths": [[4], [4], [2, 1, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd_split") assert dataset.to_dict() == expected_output def test_with_overlong_two_coluns(self): examples = { "col1": [[1, -2, 3, -4, 5, -6], [7, -8, 9], [-10, 11, -12], [13, -14, 15, -16]], "col2": [[-1, 2, -3, 4, -5, 6], [-7, 8, -9], [10, -11, 12], [-13, 14, -15, 16]], } dataset = Dataset.from_dict(examples) seq_length = 4 expected_output = { "col1": [[1, -2, 3, -4], [13, -14, 15, -16], [7, -8, 9], [-10, 11, -12], [5, -6]], "col2": [[-1, 2, -3, 4], [-13, 14, -15, 16], [-7, 8, -9], [10, -11, 12], [-5, 6]], "seq_lengths": [[4], [4], [3], [3], [2]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd_split") assert dataset.to_dict() == expected_output def test_with_non_power_of_2(self): examples = { "input_ids": [[1, 2, 3, 4, 5], [6], [7, 8, 9, 10], [11, 12, 13]], } dataset = Dataset.from_dict(examples) seq_length = 5 expected_output = { "input_ids": [[1, 2, 3, 4, 5], [7, 8, 9, 10, 6], [11, 12, 13]], "seq_lengths": [[5], [4, 1], [3]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd_split") assert dataset.to_dict() == expected_output def test_default_no_split(self): """Test default 'bfd' strategy for SFT datasets (truncates overflow).""" examples = { "input_ids": [[1, 2, 3, 4, 5], [6, 7], [8, 9, 10, 11], [12]], } dataset = Dataset.from_dict(examples) seq_length = 4 # With default 'bfd' strategy, overflow tokens are discarded expected_output = { "input_ids": [[1, 2, 3, 4], [8, 9, 10, 11], [6, 7, 12]], "seq_lengths": [[4], [4], [2, 1]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd") assert dataset.to_dict() == expected_output def test_with_empty_sequences(self): examples = { "input_ids": [[1, 2], [], [3, 4, 5], [], [6]], } dataset = Dataset.from_dict(examples) seq_length = 4 expected_output = { "input_ids": [[3, 4, 5, 6], [1, 2]], "seq_lengths": [[3, 1], [2]], } dataset = pack_dataset(dataset, seq_length, strategy="bfd_split") assert dataset.to_dict() == expected_output class TestTruncateExamples(TrlTestCase): def test_with_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples) dataset = dataset.with_format("numpy", dtype="float32") format = dataset.format max_length = 2 expected_output = { "input_ids": [[1, 2], [4, 5], [8]], "attention_mask": [[0, 1], [0, 0], [1]], } dataset = truncate_dataset(dataset, max_length) assert dataset.to_dict() == expected_output assert format == dataset.format def test_with_iterable_dataset(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples).to_iterable_dataset() dataset = dataset.with_format("numpy") formatting = dataset._formatting max_length = 2 expected_output = { "input_ids": [[1, 2], [4, 5], [8]], "attention_mask": [[0, 1], [0, 0], [1]], } dataset = truncate_dataset(dataset, max_length) num_examples = len(examples[next(iter(examples))]) assert next(iter(dataset.with_format(None).batch(batch_size=num_examples))) == expected_output assert formatting == dataset._formatting def test_with_extra_column(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], "my_column": ["a", "b", "c"], } dataset = Dataset.from_dict(examples) max_length = 2 expected_output = { "input_ids": [[1, 2], [4, 5], [8]], "attention_mask": [[0, 1], [0, 0], [1]], "my_column": ["a", "b", "c"], } dataset = truncate_dataset(dataset, max_length) assert dataset.to_dict() == expected_output def test_with_keep_end(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples) expected_output = { "input_ids": [[2, 3], [6, 7], [8]], "attention_mask": [[1, 1], [1, 1], [1]], } dataset = truncate_dataset(dataset, max_length=2, truncation_mode="keep_end") assert dataset.to_dict() == expected_output def test_with_keep_end_and_zero_max_length(self): examples = { "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], } dataset = Dataset.from_dict(examples) expected_output = { "input_ids": [[], [], []], "attention_mask": [[], [], []], } dataset = truncate_dataset(dataset, max_length=0, truncation_mode="keep_end") assert dataset.to_dict() == expected_output class TestMaybeConvertToChatML(TrlTestCase): def test_with_conversations_key(self): # Particular case where the key is "conversations": we rename it to "messages" example = { "conversations": [ {"from": "user", "value": "What color is the sky?"}, {"from": "assistant", "value": "It is blue."}, ] } expected_output = { "messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] } assert maybe_convert_to_chatml(example) == expected_output def test_without_conversations_key(self): # Same as before, but we don't rename the keys example = { "prompt": [{"from": "user", "value": "What color is the sky?"}], "completion": [{"from": "assistant", "value": "It is blue."}], } expected_output = { "prompt": [{"role": "user", "content": "What color is the sky?"}], "completion": [{"role": "assistant", "content": "It is blue."}], } assert maybe_convert_to_chatml(example) == expected_output def test_not_conversional(self): # When not needed, the example should remain unchanged example = {"text": "The sky is blue."} assert maybe_convert_to_chatml(example) == example def test_already_chatml(self): # When the example is already in ChatML format, it should remain unchanged example = { "messages": [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] } assert maybe_convert_to_chatml(example) == example ================================================ FILE: tests/test_dpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest import torch import transformers from datasets import load_dataset from packaging.version import Version from packaging.version import parse as parse_version from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig from transformers.utils import is_peft_available from trl import DPOConfig, DPOTrainer from trl.trainer.dpo_trainer import DataCollatorForPreference, DataCollatorForVisionPreference from .testing_utils import ( TrlTestCase, require_ampere_or_newer, require_bitsandbytes, require_kernels, require_liger_kernel, require_peft, require_vision, ) if is_peft_available(): from peft import LoraConfig, get_peft_model class TestDataCollatorForPreference(TrlTestCase): def test_padding_and_masks(self): collator = DataCollatorForPreference(pad_token_id=0) examples = [ {"prompt_ids": [1, 2, 3], "chosen_ids": [4, 5], "rejected_ids": [6]}, {"prompt_ids": [7, 8], "chosen_ids": [9, 10], "rejected_ids": [11, 12, 13]}, ] result = collator(examples) expected_input_ids = torch.tensor( [ [1, 2, 3, 4, 5], # prompt + chosen (example 1) [7, 8, 9, 10, 0], # prompt + chosen (example 2, padded) [1, 2, 3, 6, 0], # prompt + rejected (example 1, padded) [7, 8, 11, 12, 13], # prompt + rejected (example 2) ] ) expected_attention_mask = torch.tensor( [ [1, 1, 1, 1, 1], [1, 1, 1, 1, 0], [1, 1, 1, 1, 0], [1, 1, 1, 1, 1], ] ) expected_completion_mask = torch.tensor( [ [0, 0, 0, 1, 1], # chosen completion (example 1) [0, 0, 1, 1, 0], # chosen completion (example 2, padded) [0, 0, 0, 1, 0], # rejected completion (example 1, padded) [0, 0, 1, 1, 1], # rejected completion (example 2) ] ) assert set(result.keys()) == {"input_ids", "attention_mask", "completion_mask"} torch.testing.assert_close(result["input_ids"], expected_input_ids) torch.testing.assert_close(result["attention_mask"], expected_attention_mask) torch.testing.assert_close(result["completion_mask"], expected_completion_mask) def test_optional_reference_logps(self): collator = DataCollatorForPreference(pad_token_id=0) examples = [ { "prompt_ids": [1, 2], "chosen_ids": [3], "rejected_ids": [4], "ref_chosen_logps": 0.1, "ref_rejected_logps": 0.2, }, { "prompt_ids": [5], "chosen_ids": [6, 7], "rejected_ids": [8, 9], "ref_chosen_logps": 0.3, "ref_rejected_logps": 0.4, }, ] result = collator(examples) expected_ref_chosen_logps = torch.tensor([0.1, 0.3]) expected_ref_rejected_logps = torch.tensor([0.2, 0.4]) assert set(result.keys()) == { "input_ids", "attention_mask", "completion_mask", "ref_chosen_logps", "ref_rejected_logps", } torch.testing.assert_close(result["ref_chosen_logps"], expected_ref_chosen_logps) torch.testing.assert_close(result["ref_rejected_logps"], expected_ref_rejected_logps) def test_with_pad_to_multiple_of(self): collator = DataCollatorForPreference(pad_token_id=0, pad_to_multiple_of=5) examples = [ {"prompt_ids": [1], "chosen_ids": [2], "rejected_ids": [3]}, {"prompt_ids": [4, 5], "chosen_ids": [6, 7], "rejected_ids": [8, 9]}, ] result = collator(examples) expected_input_ids = torch.tensor( [ [1, 2, 0, 0, 0], # prompt + chosen (example 1, padded to multiple of 5) [4, 5, 6, 7, 0], # prompt + chosen (example 2) [1, 3, 0, 0, 0], # prompt + rejected (example 1, padded to multiple of 5) [4, 5, 8, 9, 0], # prompt + rejected (example 2) ] ) assert set(result.keys()) == {"input_ids", "attention_mask", "completion_mask"} torch.testing.assert_close(result["input_ids"], expected_input_ids) class TestDataCollatorForVisionPreference(TrlTestCase): @pytest.mark.skipif( Version(transformers.__version__) < Version("5.3.0"), reason="mm_token_type_ids are returned by default since transformers-5.3.0 (see transformers#43972)", ) @require_vision def test_mm_token_type_ids_shape(self): # Regression test: when the processor returns mm_token_type_ids (e.g. Qwen2.5-VL after # transformers#43972), the collator must concatenate it with zeros for the completion part # so that its shape matches input_ids. Without the fix this raises an IndexError in the model. from PIL import Image from transformers import AutoProcessor processor = AutoProcessor.from_pretrained("trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration") collator = DataCollatorForVisionPreference(processor) image = Image.new("RGB", (16, 16)) examples = [ { "images": [image], "prompt": [{"role": "user", "content": "What is this?"}], "chosen": [{"role": "assistant", "content": "A red square."}], "rejected": [{"role": "assistant", "content": "A blue circle."}], } ] output = collator(examples) assert "mm_token_type_ids" in output assert output["mm_token_type_ids"].shape == output["input_ids"].shape, ( f"mm_token_type_ids shape {output['mm_token_type_ids'].shape} != " f"input_ids shape {output['input_ids'].shape}" ) class TestDPOTrainer(TrlTestCase): @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "trl-internal-testing/tiny-Qwen3MoeForCausalLM", "trl-internal-testing/tiny-GptOssForCausalLM", ], ) def test_train(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer(model=model_id, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # Special case for harmony def test_train_gpt_oss(self): # Get the dataset dataset = load_dataset("trl-internal-testing/harmony", "preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-GptOssForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_model(self): # Instantiate the model model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32", ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize( "loss_type", [ "sigmoid", "hinge", "ipo", "exo_pair", "nca_pair", "robust", "bco_pair", "sppo_hard", "aot", "aot_unpaired", "apo_zero", "apo_down", "discopop", "sft", ], ) def test_train_loss_types(self, loss_type): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, loss_type=loss_type, label_smoothing=1e-3 if loss_type == "exo_pair" else 0.0, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", eval_strategy="steps", eval_steps=3, ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_multi_loss_types(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates loss_type=["sigmoid", "bco_pair", "sft"], # this specific combination is used in MPO report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_wpo(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", use_weighting=True, ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_ld(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ld_alpha=0.5, ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize( "f_divergence_type", ["reverse_kl", "forward_kl", "js_divergence", "alpha_divergence"], ) def test_train_with_f_divergence(self, f_divergence_type): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", f_divergence_type=f_divergence_type, ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_explicit_ref_model(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, report_to="none", ) # When specifying a ref model, it's usually because we want it to be a different checkpoint, but for testing # purposes we will just just use the same checkpoint ref_model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32" ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", ref_model=ref_model, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" new_ref_param = trainer.ref_model.get_parameter(n) torch.testing.assert_close(param, new_ref_param), f"Reference model parameter {n} has changed" def test_training_with_sync_ref_model(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates sync_ref_model=True, ref_model_sync_steps=2, # reduce sync steps to ensure a sync happens report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} assert trainer.ref_model is not None previous_ref_params = {n: param.clone() for n, param in trainer.ref_model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." new_ref_param = trainer.ref_model.get_parameter(n) assert not torch.equal(previous_ref_params[n], new_ref_param), f"Ref Parameter {n} has not changed." def test_train_model_dtype(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, model_init_kwargs={"dtype": torch.float16}, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): # For some reasonn model.layers.0.input_layernorm.weight doesn't change in GitHub Actions but does # locally. We ignore this parameter for now if "layernorm" in n: continue new_param = trainer.model.get_parameter(n) # Check the torch dtype assert new_param.dtype == torch.float16 assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_dense_with_peft_config_lora(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=1.0, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_moe_with_peft_config(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-GptOssForCausalLM" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(target_parameters=["mlp.experts.down_proj", "mlp.experts.gate_up_proj"]), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_peft_model(self): # Get the base model model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") # Get the base model parameter names base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Turn the model into a peft model lora_config = LoraConfig() model = get_peft_model(model, lora_config) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=1.0, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n and "ref" not in n: # and the peft params to be different (except base and ref) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # In practice, this test is the same as `test_train_dense_with_peft_config_lora`, since gradient checkpointing is # enabled by default in `DPOTrainer`. We keep it as a regression guard: if the default ever changes, we still # explicitly test PEFT + gradient checkpointing, which has caused issues in the past. @require_peft def test_train_with_peft_config_and_gradient_checkpointing(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates gradient_checkpointing=True, report_to="none", ) trainer = DPOTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_liger_kernel def test_train_with_liger(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates use_liger_kernel=True, report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_iterable_dataset(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train", streaming=True) # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_steps=3, report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_kernels @require_ampere_or_newer # Flash attention 2 requires Ampere or newer GPUs def test_train_padding_free(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates padding_free=True, model_init_kwargs={"attn_implementation": "kernels-community/flash-attn2"}, bf16=True, # flash_attention_2 only supports bf16 and fp16 report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_chat_template_kwargs(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") # The following template is a simplified version of the Qwen chat template, where an additional argument # `role_capital` is used to control the capitalization of roles. tokenizer.chat_template = '{%- if messages[0]["role"] == "system" -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\n" + messages[0]["content"] + "<|im_end|>\\n" }}{%- else -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n" }}{%- endif -%}{%- for message in messages -%} {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) -%} {{ "<|im_start|>" + (message.role.upper() if role_capital else message.role) + "\\n" + message.content + "<|im_end|>\\n" }} {%- elif message.role == "assistant" -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") }} {%- if message.content -%} {{ "\\n" + message.content }} {%- endif -%} {{ "<|im_end|>\\n" }} {%- elif message.role == "tool" -%} {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") -%} {{ "<|im_start|>" + ("USER" if role_capital else "user") }} {%- endif -%} {{ "\\n\\n" + message.content + "\\n" }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") -%} {{ "<|im_end|>\\n" }} {%- endif -%} {%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") + "\\n" }}{%- endif -%}' dataset = dataset.add_column( "chat_template_kwargs", [{"role_capital": bool(i % 2)} for i in range(len(dataset))] ) assert "chat_template_kwargs" in dataset.features trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, processing_class=tokenizer, ) # Assert trainer uses the same chat template as tokenizer assert trainer.processing_class.chat_template == tokenizer.chat_template # Assert chat_template is applied for i in range(2): role = "SYSTEM" if i else "system" system_prompt = ( f"<|im_start|>{role}\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>" ) system_prompt_ids = trainer.processing_class(system_prompt)["input_ids"] assert trainer.train_dataset[i]["prompt_ids"][: len(system_prompt_ids)] == system_prompt_ids # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_toolcall_data(self): # Get the dataset dataset = load_dataset("trl-internal-testing/toolcall", "preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_eval(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference") # Initialize the trainer training_args = DPOConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Train the model trainer.train() # Check that the eval loss is not None assert trainer.state.log_history[0]["eval_loss"] is not None def test_train_with_multiple_eval_dataset(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference") # Initialize the trainer training_args = DPOConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset={"data1": dataset["test"], "data2": dataset["test"]}, ) # Train the model trainer.train() # Check that the eval losses are not None assert trainer.state.log_history[-3]["eval_data1_loss"] is not None assert trainer.state.log_history[-2]["eval_data2_loss"] is not None def test_train_with_compute_metrics(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference") def dummy_compute_metrics(eval_pred): return {"my_metric": 0.123} # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], compute_metrics=dummy_compute_metrics, ) # Train the model trainer.train() # Check that the custom metric is logged assert trainer.state.log_history[-2]["eval_my_metric"] == 0.123 # In practice, this test is the same as `test_train`, since gradient checkpointing is enabled by default in # `DPOTrainer`. We keep it as a regression guard: if the default ever changes, we still explicitly test gradient # checkpointing, which has caused issues in the past. def test_train_with_gradient_checkpointing(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates gradient_checkpointing=True, report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_tag_added(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, ) for tag in ["dpo", "trl"]: assert tag in trainer.model.model_tags @require_peft def test_tag_added_peft(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, peft_config=LoraConfig(), ) for tag in ["dpo", "trl"]: assert tag in trainer.model.model_tags @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", # "trl-internal-testing/tiny-Idefics2ForConditionalGeneration", high memory peak, skipped for now # "trl-internal-testing/tiny-Idefics3ForConditionalGeneration", high memory peak, skipped for now "trl-internal-testing/tiny-LlavaForConditionalGeneration", "trl-internal-testing/tiny-LlavaNextForConditionalGeneration", "trl-internal-testing/tiny-Qwen2VLForConditionalGeneration", "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", # "trl-internal-testing/tiny-SmolVLMForConditionalGeneration", seems not to support bf16 properly pytest.param( "trl-internal-testing/tiny-Qwen3VLForConditionalGeneration", marks=[ pytest.mark.skipif( Version(transformers.__version__) < Version("4.57.0"), reason="Qwen3-VL series were introduced in transformers-4.57.0", ), pytest.mark.xfail( Version(transformers.__version__) >= Version("5.0.0"), reason="Blocked by upstream transformers bug (transformers#43334)", ), ], ), pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.2.0"), reason="Qwen3.5 models were introduced in transformers-5.2.0", ), ), ], ) @require_vision def test_train_vlm(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-image", "conversational_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, max_length=None, # for VLMs, truncating can remove image tokens, leading to errors per_device_train_batch_size=2, # VLM training is memory intensive, reduce batch size to avoid OOM report_to="none", ) trainer = DPOTrainer(model=model_id, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # For some reason, these params are not updated. This is probably not related to TRL, but to # the model itself. We should investigate this further, but for now we just skip these params. # fmt: off if ( model_id == "trl-internal-testing/tiny-Gemma3ForConditionalGeneration" and "model.vision_tower.vision_model.head" in n or model_id == "trl-internal-testing/tiny-LlavaForConditionalGeneration" and "model.vision_tower.vision_model.post_layernorm" in n or model_id == "trl-internal-testing/tiny-LlavaForConditionalGeneration" and "vision_tower.vision_model.encoder.layers.1" in n or model_id == "trl-internal-testing/tiny-LlavaNextForConditionalGeneration" and "model.vision_tower.vision_model.post_layernorm" in n or model_id == "trl-internal-testing/tiny-LlavaNextForConditionalGeneration" and "vision_tower.vision_model.encoder.layers.1" in n or model_id == "trl-internal-testing/tiny-Qwen3VLForConditionalGeneration" and "model.visual.deepstack_merger_list" in n ): # fmt: on continue assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @pytest.mark.xfail( parse_version(transformers.__version__) < parse_version("4.57.0"), reason="Mixing text-only and image+text examples is only supported in transformers >= 4.57.0", strict=False, ) @require_vision def test_train_vlm_multi_image(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-multi-image", "conversational_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_length=None, # for VLMs, truncating can remove image tokens, leading to errors per_device_train_batch_size=1, # VLM training is memory intensive, reduce batch size to avoid OOM report_to="none", ) trainer = DPOTrainer( model=model_id, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" # Gemma 3n uses a timm encoder, making it difficult to create a smaller variant for testing. # To ensure coverage, we run tests on the full model but mark them as slow to exclude from default runs. @pytest.mark.slow @require_vision @pytest.mark.skip(reason="Model google/gemma-3n-E2B-it is gated and requires HF token") def test_train_vlm_gemma_3n(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-image", "conversational_preference", split="train") # Initialize the trainer training_args = DPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_length=None, # for VLMs, truncating can remove image tokens, leading to errors per_device_train_batch_size=1, # VLM training is memory intensive, reduce batch size to avoid OOM model_init_kwargs={"dtype": "bfloat16"}, report_to="none", ) trainer = DPOTrainer(model="google/gemma-3n-E2B-it", args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if "model.audio_tower" in n or "model.embed_audio" in n: # The audio embedding parameters are not updated because this dataset contains no audio data continue assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @pytest.mark.parametrize( "dataset_config", ["conversational_preference", "standard_preference"], ) @require_vision def test_train_vlm_text_only_data(self, model_id, dataset_config): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", dataset_config, split="train") # Initialize the trainer training_args = DPOConfig(output_dir=self.tmp_dir, report_to="none") trainer = DPOTrainer( model=model_id, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n.startswith("model.visual"): torch.testing.assert_close(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is updated" else: assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @require_vision def test_train_vlm_with_max_length(self): # Regression test for #5283: mm_token_type_ids must be truncated alongside input_ids when max_length is set, # otherwise a shape mismatch crashes the model forward pass. # max_length=37 truncates 1 completion token (total_len=38) while keeping all image tokens (prompt_len=34) safe. dataset = load_dataset("trl-internal-testing/zen-image", "conversational_preference", split="train") training_args = DPOConfig( output_dir=self.tmp_dir, max_length=37, # total_len=38, prompt_len=34 — truncates completion, not image tokens per_device_train_batch_size=2, report_to="none", ) trainer = DPOTrainer( model="trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", args=training_args, train_dataset=dataset, ) trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None @require_peft @require_bitsandbytes def test_peft_with_quantization(self): # Get the base model model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, ) model = AutoModelForCausalLM.from_pretrained( model_id, dtype="float32", quantization_config=quantization_config, ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_preference", split="train") # Initialize the trainer with the already configured PeftModel training_args = DPOConfig(output_dir=self.tmp_dir, learning_rate=0.1, report_to="none") trainer = DPOTrainer(model=model, args=training_args, train_dataset=dataset, peft_config=LoraConfig()) # Save initial parameters to check they change during training previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training completed successfully assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["mean_token_accuracy"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # In bitsandbytes, bias parameters are automatically cast to the input dtype during the forward pass if # their dtype doesn’t match. This causes the module to change unexpectedly during the first forward pass of # the training. To handle this, we cast these specific bias parameters to float32 before comparison. # https://github.com/bitsandbytes-foundation/bitsandbytes/blob/45553f7392e524eacf400b132cfe01261f6477be/bitsandbytes/nn/modules.py#L518 # We still need to investigate why the compute dtype ends up being different than for these parameters. if n in [ "base_model.model.model.layers.1.self_attn.k_proj.bias", "base_model.model.model.layers.1.self_attn.q_proj.base_layer.bias", "base_model.model.model.layers.1.self_attn.v_proj.base_layer.bias", ]: param = param.float() if "lora" not in n: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "lora" in n: # We expect the peft parameters to be different assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" else: raise ValueError(f"Unexpected parameter {n} in model: {trainer.model}") @require_vision def test_train_vlm_keep_end_raises(self): # Regression test for #5285: keep_end with a VLM must raise at init time, not silently corrupt training. # Image tokens live at the start of the sequence (in the prompt); keep_end would drop them. dataset = load_dataset("trl-internal-testing/zen-image", "conversational_preference", split="train") training_args = DPOConfig( output_dir=self.tmp_dir, max_length=32, truncation_mode="keep_end", report_to="none", ) with pytest.raises(ValueError, match="truncation_mode='keep_end' is not supported for vision-language models"): DPOTrainer( model="trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", args=training_args, train_dataset=dataset, ) ================================================ FILE: tests/test_grpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gc import os import warnings from collections.abc import Callable from types import SimpleNamespace from unittest.mock import MagicMock, patch import numpy as np import pytest import torch import transformers from accelerate.utils.memory import release_memory from datasets import Dataset, Features, Image, Value, load_dataset from packaging.version import Version from transformers import ( AutoModelForCausalLM, AutoModelForImageTextToText, AutoModelForSequenceClassification, AutoProcessor, AutoTokenizer, BitsAndBytesConfig, ) from transformers.testing_utils import backend_empty_cache, torch_device from transformers.utils import is_peft_available from trl import GRPOConfig, GRPOTrainer from trl.import_utils import is_liger_kernel_available from trl.trainer.utils import get_kbit_device_map from .testing_utils import ( TrlTestCase, require_ampere_or_newer, require_bitsandbytes, require_jmespath, require_kernels, require_liger_kernel, require_peft, require_torch_accelerator, require_vision, require_vllm, ) if is_peft_available(): from peft import LoraConfig, PeftModel, get_peft_model def multiply_tool(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b async def async_multiply_tool(a: int, b: int) -> int: """ Asynchronously multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b class TestGetHighEntropyMask(TrlTestCase): def get_high_entropy_mask(self, entropies, mask, threshold): """Helper method to test the get_high_entropy_mask functionality.""" # Create a mock trainer with minimal setup from unittest.mock import Mock # Create a mock accelerator mock_accelerator = Mock() mock_accelerator.num_processes = 1 # Single process for testing # Create a minimal trainer instance just to access the method trainer = Mock(spec=GRPOTrainer) trainer.accelerator = mock_accelerator trainer.accelerator.gather = lambda x: x trainer.accelerator.pad_across_processes = lambda x, dim, pad_index: x # Call the actual method from GRPOTrainer return GRPOTrainer.get_high_entropy_mask(trainer, entropies, mask, threshold) def test_compute_entropy_mask_0(self): # We have a total of 12 tokens out of which 10 are non-pad. # for a top_entropy_quantile of 0.8, we expect the top 20% i.e 2 non-pad tokens corresponding to # the highest entropy to be unmasked. # In our example these will be the tokens corresponding to the entropies 0.9 and 1.0 since 1.1 and 1.2 are pad # tokens they are excluded from the entropy threshold calculation. entropies = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]) mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=0.8) expected_mask = torch.tensor([[0, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) def test_compute_entropy_mask_1(self): # Another example with a different set of entropies and a different mask. entropies = torch.tensor([[0.1, 0.2, 0.3, 1.4, 0.5, 0.14], [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]]) mask = torch.tensor([[1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=0.8) expected_mask = torch.tensor([[0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) def test_compute_entropy_mask_lower_threshold(self): # For a threshold of 0.5 we expect the top half of the non-pad tokens to be unmasked. entropies = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]) mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=0.5) expected_mask = torch.tensor([[0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) def test_compute_entropy_threshold_0(self): # If the threshold is 0.0 then we expect the mask to be all ones for non-pad tokens. entropies = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]) mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=0.0) expected_mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) def test_compute_entropy_threshold_1(self): # If the threshold is 1.0 then we expect the mask to be all zeros BUT ONE VALUE. entropies = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]) mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=1.0) expected_mask = torch.tensor([[0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) def test_compute_entropy_all_masked(self): # If there are no non-pad tokens we expect the mask to be all zeros. entropies = torch.tensor([[0.1, 0.2, 0.3, 0.4, 0.5, 0.6], [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]]) mask = torch.tensor([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) entropy_mask = self.get_high_entropy_mask(entropies, mask, threshold=0.5) expected_mask = torch.tensor([[0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]], dtype=torch.bool) torch.testing.assert_close(entropy_mask, expected_mask) class TestGRPORolloutDispatch: def _make_trainer(self): trainer = object.__new__(GRPOTrainer) trainer.accelerator = SimpleNamespace( device=torch.device("cpu"), is_main_process=True, gather=lambda t: t, ) trainer.args = SimpleNamespace(report_to=[]) trainer.model = SimpleNamespace(training=True) trainer.state = SimpleNamespace(global_step=2, num_input_tokens_seen=0) trainer._last_loaded_step = 1 trainer.use_vllm = False trainer.use_transformers_paged = False trainer.vllm_generation = SimpleNamespace(sync_weights=MagicMock()) trainer.processing_class = SimpleNamespace( batch_decode=MagicMock(return_value=["decoded"]), ) trainer.tools = None trainer.eos_token_id = 2 trainer.pad_token_id = 0 trainer._metrics = { "train": { "num_tokens": [], **{ k: [] for k in [ "completions/mean_length", "completions/min_length", "completions/max_length", "completions/clipped_ratio", "completions/mean_terminated_length", "completions/min_terminated_length", "completions/max_terminated_length", ] }, } } return trainer def test_generate_prefers_rollout_func(self): trainer = self._make_trainer() trainer.rollout_func = MagicMock( return_value={ "prompt_ids": [[1]], "completion_ids": [[2]], "logprobs": [[-0.1]], "env_mask": [[1]], } ) result = trainer._generate(["prompt"]) assert result[0] == [[1]] # prompt_ids assert result[1] == [[2]] # completion_ids assert result[2] == [[1]] # tool_mask (from env_mask) trainer.rollout_func.assert_called_once_with(["prompt"], trainer) def test_generate_rollout_func_syncs_vllm_weights_when_needed(self): trainer = self._make_trainer() trainer.use_vllm = True trainer.rollout_func = MagicMock( return_value={"prompt_ids": [[1]], "completion_ids": [[2]], "logprobs": [[0.0]]} ) trainer._generate(["prompt"]) trainer.vllm_generation.sync_weights.assert_called_once() assert trainer._last_loaded_step == trainer.state.global_step trainer.rollout_func.assert_called_once_with(["prompt"], trainer) def test_generate_rollout_func_raises_when_required_keys_are_missing(self): trainer = self._make_trainer() trainer.rollout_func = MagicMock(return_value={"prompt_ids": [[1]], "completion_ids": [[2]]}) with pytest.raises(ValueError, match="rollout_func must return keys"): trainer._generate(["prompt"]) class TestGRPOTrainer(TrlTestCase): def test_init_minimal(self): # Test that GRPOTrainer can be instantiated with only model, reward_model and train_dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", train_dataset=dataset, ) @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_training(self, config_name): dataset = load_dataset("trl-internal-testing/zen", config_name, split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize("loss_type", ["bnpo", "dr_grpo", "dapo", "cispo", "sapo", "luspo", "vespo"]) def test_training_loss_types(self, loss_type): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, importance_sampling_level="sequence" if loss_type == "luspo" else "token", learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=32, # reduce the completion length to reduce memory usage gradient_accumulation_steps=2, # set to 2 to test than DAPO can operate with accumulated batch loss_type=loss_type, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_eval(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage per_device_eval_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage eval_strategy="steps", eval_steps=2, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() def test_training_with_num_generations_eval(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage per_device_eval_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_generations_eval=1, eval_strategy="steps", eval_steps=2, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() # Regression test for eval_on_start with loss_type="grpo" (one of the loss types that depends on # current_gradient_accumulation_steps): evaluation runs before the first training step, when that value is still # unset. Previously this caused the initial eval to crash. def test_training_eval_on_start(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage per_device_eval_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage loss_type="grpo", eval_strategy="steps", eval_steps=2, eval_on_start=True, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() def test_training_multiple_iterations(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_iterations=2, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_peft def test_training_peft_config(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @require_peft def test_training_peft_model(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] lora_config = LoraConfig() model = get_peft_model(model, lora_config) dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n and "ref" not in n: # and the peft params to be different (except base and ref) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." # In practice, this test is the same as `test_training_peft_config`, since gradient checkpointing is enabled by # default in `GRPOTrainer`. We keep it as a regression guard: if the default ever changes, we still explicitly test # PEFT + gradient checkpointing, which has caused issues in the past. @require_peft def test_training_peft_with_gradient_checkpointing(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage gradient_checkpointing=True, # enable gradient checkpointing report_to="none", ) trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." def test_training_different_reward_model(self): # Use a reward model different from the model: different chat template, tokenization, etc. dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") reward_model_id = "trl-internal-testing/tiny-LlamaForSequenceClassification-3.2" reward_model = AutoModelForSequenceClassification.from_pretrained(reward_model_id) reward_tokenizer = AutoTokenizer.from_pretrained(reward_model_id) # By default, the trainer uses the eos token as the padding token. However, for Llama models, the eos token # appears in the chat template. Using it as a pad token disrupts the reward calculation, as the calculation # considers the score of the last token before the first pad token. To ensure correct reward calculations, # we use a separate pad token instead. reward_tokenizer.pad_token = "<|finetune_right_pad_id|>" training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_model, args=training_args, train_dataset=dataset, reward_processing_classes=reward_tokenizer, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_standard(self): # Test if trainer can handle reward function with standard format dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_conversational(self): # Test if trainer can handle reward function with conversational format dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that gives higher scores to longer completion content.""" completion_contents = [completion[0]["content"] for completion in completions] return [float(len(content)) for content in completion_contents] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs(self): # Test that GRPOTrainer can be instantiated with multiple reward functions dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def reward_func2(completions, **kwargs): """Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func1, reward_func2], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_sync_and_async_reward_funcs(self): # Test that GRPOTrainer can be instantiated with multiple reward functions one of which is async dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def sync_reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def sync_reward_func2(completions, **kwargs): return [1 for _ in completions] async def async_reward_func(completions, **kwargs): """Async Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[sync_reward_func1, sync_reward_func2, async_reward_func], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs_with_None_output(self): """Test that a valid math reward function is processed correctly while the code reward function returns None.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def applicable_reward_func(completions, **kwargs): """A reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def non_applicable_reward_func(completions, **kwargs): """A reward function that returns None for all inputs, as it is not applicable to this sample.""" return [None] * len(completions) training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[ applicable_reward_func, non_applicable_reward_func, ], # One applicable, one non applicable args=training_args, train_dataset=dataset, ) previous_trainable_params = { n: param.clone() for n, param in trainer.model.named_parameters() if param.requires_grad } trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs_with_weights(self): """Test that GRPOTrainer can handle multiple reward functions with weights.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def reward_func2(completions, **kwargs): """Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", reward_weights=[0.7, 0.3], # weight of reward_func1 and reward_func2 respectively ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func1, reward_func2], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training logs contain both reward metrics assert trainer.state.log_history[-1]["train_loss"] is not None assert "rewards/reward_func1/mean" in trainer.state.log_history[-1] assert "rewards/reward_func1/std" in trainer.state.log_history[-1] assert "rewards/reward_func2/mean" in trainer.state.log_history[-1] assert "rewards/reward_func2/std" in trainer.state.log_history[-1] # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_mixed_reward_funcs(self): # Test if the trainer can handle a mix of reward functions and reward models dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func, "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5"], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_additional_column(self): # Test if trainer can handle reward function that rely on additional columns in the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Add a column to the dataset (dummy example, the column could be anything) some_values = list(range(len(dataset))) dataset = dataset.add_column("some_values", some_values) def reward_func(completions, some_values, **kwargs): """Reward function that rewards completions with lengths closer to the values in some_values.""" return [ float(abs(len(completion) - value)) for completion, value in zip(completions, some_values, strict=True) ] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_sync_ref_model(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, beta=0.1, # ensure ref model is created so sync can update it learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage sync_ref_model=True, ref_model_sync_steps=2, # reduce sync steps to ensure a sync happens report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} assert trainer.ref_model is not None previous_ref_params = {n: param.clone() for n, param in trainer.ref_model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." new_ref_param = trainer.ref_model.get_parameter(n) assert not torch.equal(previous_ref_params[n], new_ref_param), f"Ref Parameter {n} has not changed." def test_training_beta_non_zero(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, beta=0.1, # set beta to non-zero value to test the case where the reference model is used learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_pad_to_multiple_of(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage pad_to_multiple_of=8, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_get_off_policy_mask(self): """ Test the logic of off-policy masking: - Keep if Advantage >= 0 - Keep if KL <= threshold - Drop if Advantage < 0 AND KL > threshold """ mask = torch.ones((3, 4)) # B=3 sequences, T=4 tokens advantages = torch.tensor([1.0, -1.0, -1.0]).unsqueeze(-1) sampling_per_token_logps = torch.zeros((3, 4)) per_token_logps = torch.zeros((3, 4)) per_token_logps[0, :] = -2.0 # Pos adv + High KL (0−(−2)=2) -> Keep per_token_logps[1, :] = -0.5 # Neg adv + Low KL (0.5) -> Keep per_token_logps[2, :] = -2.0 # Neg adv + High KL (2.0) -> Drop off_policy_threshold = 1.0 expected_mask = torch.tensor([[1.0], [1.0], [0.0]]) off_policy_mask = GRPOTrainer.get_off_policy_mask( advantages, per_token_logps, sampling_per_token_logps, mask, off_policy_threshold ) torch.testing.assert_close(off_policy_mask, expected_mask) def test_get_off_policy_mask_padding(self): """Test that padding is correctly ignored in KL calculation.""" mask = torch.tensor([[1.0, 1.0, 0.0, 0.0]]) # 2 valid tokens advantages = torch.tensor([[-1.0]]) # Negative advantage sampling_per_token_logps = torch.zeros((1, 4)) per_token_logps = torch.zeros((1, 4)) # Valid tokens have High KL (2.0) per_token_logps[0, 0] = -2.0 per_token_logps[0, 1] = -2.0 # Padding tokens have abnormal values (should be ignored) per_token_logps[0, 2] = -10_000.0 per_token_logps[0, 3] = 10_000.0 off_policy_threshold = 1.0 # Avg KL on valid tokens = (2+2)/2 = 2.0 > 1.0 -> Drop expected_mask = torch.tensor([[0.0]]) off_policy_mask = GRPOTrainer.get_off_policy_mask( advantages, per_token_logps, sampling_per_token_logps, mask, off_policy_threshold ) torch.testing.assert_close(off_policy_mask, expected_mask) # Now test with Low KL on valid tokens per_token_logps[0, 0] = -0.5 per_token_logps[0, 1] = -0.5 # Avg KL = 0.5 <= 1.0 -> Keep expected_mask_keep = torch.tensor([[1.0]]) off_policy_mask_keep = GRPOTrainer.get_off_policy_mask( advantages, per_token_logps, sampling_per_token_logps, mask, off_policy_threshold ) torch.testing.assert_close(off_policy_mask_keep, expected_mask_keep) def test_training_with_off_policy_mask(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, off_policy_mask_threshold=0.5, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_liger_kernel @pytest.mark.xfail(reason="Off-Policy Masking isn't compatible with Liger yet.") def test_training_with_off_policy_mask_with_liger(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, off_policy_mask_threshold=0.5, use_liger_kernel=True, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_liger_kernel def test_compute_liger_loss_passes_vllm_is_ratio(self): """Test that importance_sampling_ratio from inputs is passed to liger_grpo_loss as vllm_is_ratio.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, per_device_train_batch_size=3, num_generations=3, max_completion_length=8, use_liger_kernel=True, report_to="none", logging_strategy="no", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) # Mock _generate_and_score_completions to inject importance_sampling_ratio original_gen = trainer._generate_and_score_completions def gen_with_is_ratio(*args, **kwargs): result = original_gen(*args, **kwargs) B, T = result["completion_ids"].shape result["importance_sampling_ratio"] = torch.full((B, T), 0.5, device=result["completion_ids"].device) return result with ( patch.object(trainer, "_generate_and_score_completions", side_effect=gen_with_is_ratio), patch.object(trainer.liger_grpo_loss, "forward", wraps=trainer.liger_grpo_loss.forward) as mock_forward, ): trainer.train() # Verify vllm_is_ratio was passed in every call to liger_grpo_loss assert mock_forward.call_count > 0, "liger_grpo_loss.forward was never called" for call in mock_forward.call_args_list: vllm_is_ratio = call.kwargs.get("vllm_is_ratio") assert vllm_is_ratio is not None, ( "vllm_is_ratio should not be None when importance_sampling_ratio is present" ) assert (vllm_is_ratio == 0.5).all(), ( "vllm_is_ratio values should match the injected importance_sampling_ratio" ) release_memory(trainer.model, trainer) def test_training_with_bias_correction_kl(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, beta=0.1, # set beta to non-zero value to test the case where the reference model is used use_bias_correction_kl=True, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_name", ["trl-internal-testing/tiny-Qwen3ForCausalLM", "trl-internal-testing/tiny-Gemma2ForCausalLM"], # Gemma2 has the input word embeddings and lm_head tied, Qwen3 does not ) def test_training_with_cast_lm_head_to_fp32(self, model_name): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", cast_lm_head_to_fp32=True, ) trainer = GRPOTrainer( model=model_name, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.model.lm_head.weight.dtype == torch.float32 # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_entropy_filter(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", top_entropy_quantile=0.2, ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_peft @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_and_peft(self): """Test that training works with vLLM for generation.""" model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-0.5B-Instruct", dtype="float32" ) # tiny model is too small for vLLM base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, ) lora_config = LoraConfig( target_modules="all-linear", # test with non-default modules as it adds extra keys in state_dict that we need to handle modules_to_save=["embed_tokens", "lm_head"], ) trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n and "original_module" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_structured_outputs(self): """Test that training works with vLLM for generation with structured outputs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, vllm_structured_outputs_regex=r"\n.*\n\n\n.*\n", ) trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny model is too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_importance_sampling_correction(self): """Test that training works with vLLM for generation with structured outputs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", use_vllm=True, vllm_importance_sampling_correction=True, vllm_importance_sampling_cap=3.0, ) trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny model is too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_additional_generation_kwargs(self): """Test that training works with additional generation kwargs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", top_p=0.9, top_k=10, min_p=0.01, repetition_penalty=1.1, ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_with_additional_generation_kwargs(self): """Test that training works with vLLM and additional generation kwargs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, top_p=0.9, top_k=10, min_p=0.01, repetition_penalty=1.1, ) trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny model is too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_normalize_then_sum_aggregation(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def reward_func2(completions, **kwargs): """Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage multi_objective_aggregation="normalize_then_sum", report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func1, reward_func2], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize("scale_rewards", [False, "group", "batch", True, "none"]) def test_training_scale_rewards(self, scale_rewards): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage scale_rewards=scale_rewards, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @patch("transformers.generation.utils.GenerationMixin.generate") def test_training_with_mask_truncated_completions(self, mock_generate): """Test that training works with mask_truncated_completions=True parameter.""" # We mock the generate method because the model's random weights make it extremely unlikely to produce a # sequence containing the EOS token within the allowed max_completion_length. As a result, all tokens are # masked in the loss, the model doesn't update, and the final check (which verifies the update) fails. def fake_generate(input_ids, **kwargs): # pad_token_id = 151643; eos_token_id = 151645 completion_ids = torch.tensor( [ [1, 2, 3, 4, 5, 6, 7, 8], # this one is truncated [9, 10, 11, 151645, 151643, 151643, 151643, 151643], # this one contains eos [12, 13, 14, 15, 16, 17, 18, 151645], # particular case, eos is generated just within the limit ], device=input_ids.device, ) return torch.cat([input_ids, completion_ids], dim=1) mock_generate.side_effect = fake_generate dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage mask_truncated_completions=True, # Enable masking of truncated completions report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_mask_truncated_completions_all_masked(self): """ Test that when all generated completions are truncated (i.e., none contain an EOS token), and mask_truncated_completions=True, the model receives no effective learning signal and therefore does not update its parameters. Here, we don't mock the generate method, be we rely on the fact that the model the probability of generating the EOS token is extremely low, so all generated completions are truncated. """ dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage mask_truncated_completions=True, # Enable masking of truncated completions report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert torch.equal(param, new_param), f"Parameter {n} has changed." def test_warning_raised_all_rewards_none(self, caplog): """Test that a proper warning is raised when all rewards are None.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def always_none_reward_func(completions, **kwargs): """Reward function that always returns None.""" return [None] * len(completions) training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=always_none_reward_func, args=training_args, train_dataset=dataset, ) with caplog.at_level("WARNING", logger="trl.trainer.grpo_trainer"): trainer.train() expected_warning = "All reward functions returned None for the following kwargs:" assert expected_warning in caplog.text def test_training_num_generations_larger_than_batch_size(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_generations=6, # the number of generations is larger than the batch size, but gradient_accumulation_steps=2, # gradient accumulation should allow that report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_delta_clipping(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage delta=2.0, # set delta to a non-None value report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_dataloader_workers(self): # Pytest/CI often starts background threads before tests run. With Python 3.12, using the default "fork" start # method in a multi-threaded process emits a DeprecationWarning and may deadlock. # # We force "spawn" here to make multiprocessing safe under pytest when DataLoader workers are enabled. This is # test-environment–specific and not required by the training logic itself. # # This means the test does not cover "fork". However, "spawn" is stricter (requires full picklability and clean # state) and avoids fork-after-threads issues that pytest cannot reliably test anyway. Fork-specific behavior, # if needed, should be tested in a clean process outside pytest. torch.multiprocessing.set_start_method("spawn", force=True) dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage dataloader_num_workers=2, # use multiple dataloader workers report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_generation_kwargs(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage # Pass gen kwargs generation_kwargs={"do_sample": True, "top_k": 50, "num_beams": 2, "length_penalty": -0.1}, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_reward_func_accessing_trainer_state(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): trainer_state = kwargs.get("trainer_state") assert trainer_state is not None # transformers.TrainerState instance should have a `global_step` property. assert hasattr(trainer_state, "global_step") return [float(len(set(completion))) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() def test_training_reward_func_with_log_extra(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): log_extra = kwargs.get("log_extra") assert log_extra is not None log_extra("test_column", [completion[:5] for completion in completions]) return [float(len(completion)) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", log_completions=True, ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() assert "test_column" in trainer._logs["extra"] def test_training_reward_func_with_log_metric(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): log_metric = kwargs.get("log_metric") assert log_metric is not None log_metric("custom_accuracy", 0.75) return [float(len(completion)) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() # log_metric appends to _metrics, which gets averaged and merged into log_history logged_keys = {k for entry in trainer.state.log_history for k in entry} assert "custom_accuracy" in logged_keys def test_prepare_input_called_with_correct_data(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_completion_length=8, # reduce the completion length to reduce memory usage gradient_accumulation_steps=3, # can be anything in this test # steps_per_generation*per_device_train_batch_size=24 is divisible by num_generations=4 steps_per_generation=4, num_generations=4, per_device_train_batch_size=6, # reduce the batch size to reduce memory usage num_iterations=2, shuffle_dataset=False, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) # steps_per_generation=4, per_device_train_batch_size=6 and num_generations=4, so we expect a # generation batch of 24 samples (steps_per_generation * per_device_train_batch_size), containing 6 # different prompts (steps_per_generation * per_device_train_batch_size // num_generations), each repeated # 4 times (num_generations). expected_first_generation_batch = ( [{"prompt": "Beautiful is better than"}] * 4 + [{"prompt": "Explicit is"}] * 4 + [{"prompt": "Simple is better"}] * 4 + [{"prompt": "Complex"}] * 4 + [{"prompt": "Flat is better than"}] * 4 + [{"prompt": "Sparse is better"}] * 4 ) expected_second_generation_batch = ( [{"prompt": "Readability"}] * 4 + [{"prompt": "Special cases aren't special"}] * 4 + [{"prompt": "Although practicality beats"}] * 4 + [{"prompt": "Errors should never"}] * 4 + [{"prompt": "Unless explicitly"}] * 4 + [{"prompt": "In the face of ambiguity, refuse"}] * 4 ) with patch.object(GRPOTrainer, "training_step", wraps=trainer.training_step) as mock_prepare: trainer.train() # 3 epochs * 2 iterations * 2 generation batches to cover the dataset * 4 steps_per_generation assert mock_prepare.call_count == 48 for i in range(0, 8): # Generation batch repeated 8 times (steps_per_generation*num_iterations) assert mock_prepare.call_args_list[i].args[1] == expected_first_generation_batch for i in range(8, 16): assert mock_prepare.call_args_list[i].args[1] == expected_second_generation_batch @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", "trl-internal-testing/tiny-LlavaNextForConditionalGeneration", "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", "trl-internal-testing/tiny-Qwen2VLForConditionalGeneration", pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.2.0"), reason="Qwen3.5 models were introduced in transformers-5.2.0", ), ), # "trl-internal-testing/tiny-SmolVLMForConditionalGeneration", seems not to support bf16 properly ], ) @require_vision def test_training_vlm(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ( "model.vision_tower.", "model.multi_modal_projector.", "model.vision_model.", "model.visual.", "model.image_newline", ) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_vision def test_training_vlm_with_pad_to_multiple_of(self): # Models like Gemma3 use other forward keyword arguments like token_type_ids that also need to be padded when # using pad_to_multiple_of, so we test that the trainer correctly pads all the necessary inputs in this case. dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage pad_to_multiple_of=7, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Gemma3ForConditionalGeneration", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision def test_training_vlm_beta_non_zero(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, beta=0.1, # set beta to non-zero value to test the case where the reference model is used learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ("model.visual.",) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision @require_peft def test_training_vlm_peft(self, model_id): model = AutoModelForImageTextToText.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model, reward_funcs=reward_func, args=training_args, train_dataset=dataset, peft_config=LoraConfig(target_modules=["q_proj", "v_proj"]), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision def test_training_vlm_and_importance_sampling(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage steps_per_generation=2, # increase the steps per generation to trigger IS report_to="none", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ("model.visual.",) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ pytest.param( "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", marks=pytest.mark.xfail( (Version("5.2.0") < Version(transformers.__version__)) and not is_liger_kernel_available(min_version="0.8.0"), reason="Upstream issue tracked at https://github.com/linkedin/Liger-Kernel/issues/1117", ), ), ], ) @require_vision @require_liger_kernel def test_training_vlm_and_liger(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage use_liger_kernel=True, # enable Liger kernel report_to="none", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ("model.visual.",) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", ], ) @require_vision @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vlm_and_vllm(self, model_id) -> None: dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", use_vllm=True, vllm_mode="server", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision def test_training_vlm_multi_image(self, model_id): dataset = load_dataset("trl-internal-testing/zen-multi-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = GRPOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_sequence_importance_sampling(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_iterations=2, # the importance sampling weights won't be 0 in this case importance_sampling_level="sequence", report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_chat_template_kwargs(self): dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", chat_template_kwargs={"enable_thinking": False}, ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.xfail( condition=Version(transformers.__version__) < Version("5.0.0"), reason="Tool parsing is not supported in transformers versions below 5.0.0", strict=True, ) @require_jmespath @pytest.mark.parametrize("tools", [[multiply_tool], [async_multiply_tool]]) def test_training_with_tools(self, tools: list[Callable]): # In this test, we define a simple tool that multiplies two integers. Regardless of the input prompt, # the model will generate 3 completions, 2 of which will be valid tool calls. Among the 2 tool calls, one will # succeed and the other will fail (because of a wrong argument name). dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=128, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen3MoeForCausalLM", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, tools=tools, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} tool_name = tools[0].__name__ def fake_generate(input_ids, **kwargs): if input_ids.shape[0] == 3: # first call # fmt: off if tool_name == "multiply_tool": completion_ids = torch.tensor( [ # '\n{"name": "multiply_tool", "arguments": {"a": 3, "b": 4}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 64648, 22785, 497, 330, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 65, 788, 220, 19, 11248, 151658, 151645], # an invalid tool call with wrong argument name # '\n{"name": "multiply_tool", "arguments": {"a": 3, "c": 4}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 64648, 22785, 497, 330, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 66, 788, 220, 19, 11248, 151658, 151645], # "I don't know any tool<|im_end|>" [40, 1513, 944, 1414, 894, 5392, 151645, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643], ], device=input_ids.device, ) elif tool_name == "async_multiply_tool": completion_ids = torch.tensor( [ # '\n{"name": "async_multiply_tool", "arguments": {"a": 3, "b": 4}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 7692, 93054, 22785, 497, 330, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 65, 788, 220, 19, 11248, 151658, 151645], # an invalid tool call with wrong argument name # '\n{"name": "async_multiply_tool", "arguments": {"a": 3, "c": 4}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 7692, 93054, 22785, 497, 330, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 66, 788, 220, 19, 11248, 151658, 151645], # "I don't know any tool<|im_end|>" [40, 1513, 944, 1414, 894, 5392, 151645, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643], ], device=input_ids.device, ) # fmt: on else: # second call will only have two inputs in the batch, because two examples have a tool call. completion_ids = torch.tensor( [ # 'Done!<|im_end|>' [17453, 0, 151645], # 'Done!<|im_end|>' [17453, 0, 151645], ], device=input_ids.device, ) return torch.cat([input_ids, completion_ids], dim=-1) with patch.object(trainer.model, "generate", side_effect=fake_generate): trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["tools/call_frequency"] is not None assert trainer.state.log_history[-1]["tools/call_frequency"] == pytest.approx(2 / 3) assert trainer.state.log_history[-1]["tools/failure_frequency"] is not None assert trainer.state.log_history[-1]["tools/failure_frequency"] == pytest.approx(1 / 2) # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.xfail( condition=Version(transformers.__version__) < Version("5.2.0"), reason="Environment factory support is not available in transformers versions below 5.2.0", strict=True, ) @require_jmespath @patch.dict(os.environ, {"TRL_EXPERIMENTAL_SILENCE": "1"}) def test_training_with_environment_factory(self): # In this test, we define a simple tool that increments an internal counter. Regardless of the input prompt, # the model will generate 3 completions, 2 of which will be valid tool calls. Among the 2 tool calls, one will # succeed and the other will fail (because of a wrong tool name). dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") class DummyEnvironment: def reset(self, **kwargs): self._counter = 0 def increment(self, step: int) -> int: """ Increment the internal counter. Args: step: Value to add to the counter. Returns: The updated counter value. """ self._counter += step return self._counter training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen3MoeForCausalLM", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, environment_factory=DummyEnvironment, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} def fake_generate(input_ids, **kwargs): if input_ids.shape[0] == 3: # first call # fmt: off completion_ids = torch.tensor( [ # '\n{"name": "increment", "arguments": {"step": 1}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 35744, 497, 330, 16370, 788, 5212, 9520, 788, 220, 16, 11248, 151658, 151645, 151643], # an invalid tool call with wrong tool name # '\n{"name": "decrement", "arguments": {"step": 2}}\n<|im_end|>' [151657, 198, 4913, 606, 788, 330, 450, 13477, 497, 330, 16370, 788, 5212, 9520, 788, 220, 17, 11248, 151658, 151645], # "I won't increment<|im_end|>" [40, 2765, 944, 16252, 151645, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643], ], device=input_ids.device, ) # fmt: on else: # second call will only have two inputs in the batch, because two examples have a tool call. completion_ids = torch.tensor( [ # 'Done!<|im_end|>' [17453, 0, 151645], # 'Done!<|im_end|>' [17453, 0, 151645], ], device=input_ids.device, ) return torch.cat([input_ids, completion_ids], dim=-1) with patch.object(trainer.model, "generate", side_effect=fake_generate): trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["tools/call_frequency"] is not None assert trainer.state.log_history[-1]["tools/call_frequency"] == pytest.approx(2 / 3) assert trainer.state.log_history[-1]["tools/failure_frequency"] is not None assert trainer.state.log_history[-1]["tools/failure_frequency"] == pytest.approx(1 / 2) # Check the states of the environment assert trainer.environments[0]._counter == 1 # should have been incremented once assert trainer.environments[1]._counter == 0 # shouldn't have been incremented because the tool call failed assert trainer.environments[2]._counter == 0 # shouldn't have been incremented because no tool call was made # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.xfail( condition=Version(transformers.__version__) < Version("5.0.0"), reason="Tool parsing is not supported in transformers versions below 5.0.0", strict=True, ) @require_jmespath def test_training_with_malformed_tool_calls(self): dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=128, report_to="none", ) trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen3MoeForCausalLM", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, tools=[multiply_tool], ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} def fake_generate(input_ids, **kwargs): # If input_ids.shape[0] < 3, it means that it's a second call, which should not happen here assert input_ids.shape[0] == 3 # fmt: off completion_ids = torch.tensor( [ # '\n{"arguments": {"a": 3, "b": 4}}\n<|im_end|>' [151657, 198, 4913, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 65, 788, 220, 19, 11248, 151658, 151645, 151643, 151643, 151643, 151643, 151643], # '\n{"arguments": {"a": 3, "b": 4}}\n<|im_end|>' [27, 14172, 6659, 397, 4913, 16370, 788, 5212, 64, 788, 220, 18, 11, 330, 65, 788, 220, 19, 11248, 522, 14172, 6659, 29, 151645], # '\n{"arguments": {a: 3, b: 4}}\n<|im_end|>' [151657, 198, 4913, 16370, 788, 314, 64, 25, 220, 18, 11, 293, 25, 220, 19, 11248, 151658, 151645, 151643, 151643, 151643, 151643, 151643, 151643], ], device=input_ids.device, ) # fmt: on return torch.cat([input_ids, completion_ids], dim=-1) with patch.object(trainer.model, "generate", side_effect=fake_generate): trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_mismatched_reward_processing_classes_length(self): """Test that mismatched length between reward_funcs and reward_processing_classes raises error.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use two reward models reward_models = [ "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", "trl-internal-testing/tiny-Qwen3ForSequenceClassification", ] # Create a single processing class (tokenizer) single_processing_class = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) training_args = GRPOConfig(output_dir=self.tmp_dir, report_to="none") with pytest.raises(ValueError, match="must match"): GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_models, reward_processing_classes=single_processing_class, # only one, but need two args=training_args, train_dataset=dataset, ) def test_correct_reward_processing_classes_list(self): """Test that correct list of reward_processing_classes works properly.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use two reward models reward_models = [ "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", "trl-internal-testing/tiny-Qwen3ForSequenceClassification", ] # Create processing classes processing_class1 = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) processing_class2 = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen3ForSequenceClassification") training_args = GRPOConfig(output_dir=self.tmp_dir, report_to="none") # Correct list length should work correct_processing_classes = [processing_class1, processing_class2] trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_models, reward_processing_classes=correct_processing_classes, args=training_args, train_dataset=dataset, ) assert len(trainer.reward_processing_classes) == len(reward_models) def test_single_reward_model_with_single_processing_class(self): """Test that single reward model with single processing class works.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use single reward model reward_model = "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" # Create a single processing class (tokenizer) single_processing_class = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) training_args = GRPOConfig(output_dir=self.tmp_dir, report_to="none") trainer = GRPOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_model, reward_processing_classes=single_processing_class, # single object for single reward model args=training_args, train_dataset=dataset, ) assert len(trainer.reward_processing_classes) == 1 assert trainer.reward_processing_classes[0] == single_processing_class @pytest.mark.slow @require_torch_accelerator class TestGRPOTrainerSlow(TrlTestCase): def setup_method(self): self.train_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") self.eval_dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="test") self.max_length = 128 def teardown_method(self): gc.collect() backend_empty_cache(torch_device) gc.collect() @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_liger_kernel def test_training_with_liger_grpo_kernel(self, model_name): training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, num_generations=3, use_liger_kernel=True, max_completion_length=self.max_length, report_to="none", logging_strategy="no", ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token if tokenizer.pad_token is None else tokenizer.pad_token trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, processing_class=tokenizer, ) from liger_kernel.chunked_loss import LigerFusedLinearGRPOLoss assert isinstance(trainer.liger_grpo_loss, LigerFusedLinearGRPOLoss) previous_trainable_params = {n: param.clone() for n, param in model.named_parameters()} trainer.train() for n, param in previous_trainable_params.items(): new_param = model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." release_memory(model, trainer) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_liger_kernel @require_peft def test_training_with_liger_grpo_kernel_and_peft(self, model_name): from peft import LoraConfig, TaskType training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, num_generations=3, use_liger_kernel=True, max_completion_length=self.max_length, report_to="none", logging_strategy="no", ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token if tokenizer.pad_token is None else tokenizer.pad_token # Configure PEFT with LoRA peft_config = LoraConfig( task_type=TaskType.CAUSAL_LM, inference_mode=False, r=8, lora_alpha=32, lora_dropout=0.1, target_modules=["q_proj", "v_proj"], ) trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, processing_class=tokenizer, peft_config=peft_config, ) from liger_kernel.chunked_loss import LigerFusedLinearGRPOLoss assert isinstance(trainer.liger_grpo_loss, LigerFusedLinearGRPOLoss) # Verify PEFT adapter is properly initialized from peft import PeftModel assert isinstance(trainer.model, PeftModel), "Model should be wrapped with PEFT" # Store adapter weights before training previous_trainable_params = { n: param.clone() for n, param in trainer.model.named_parameters() if param.requires_grad } assert len(previous_trainable_params) > 0, "No trainable parameters found in PEFT model" trainer.train() # Verify adapter weights have changed after training for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." release_memory(model, trainer) @require_liger_kernel def test_liger_grpo_kernel_importance_sampling(self): model_name = "trl-internal-testing/tiny-LlamaForCausalLM-3.2" training_args = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, num_generations=3, use_liger_kernel=True, max_completion_length=self.max_length, importance_sampling_level="sequence", report_to="none", logging_strategy="no", ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token if tokenizer.pad_token is None else tokenizer.pad_token trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, processing_class=tokenizer, ) from liger_kernel.chunked_loss import LigerFusedLinearGRPOLoss assert isinstance(trainer.liger_grpo_loss, LigerFusedLinearGRPOLoss) previous_trainable_params = {n: param.clone() for n, param in model.named_parameters()} trainer.train() for n, param in previous_trainable_params.items(): new_param = model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." release_memory(model, trainer) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) def test_training_with_transformers_paged(self, model_name): """Test that training works with transformers paged implementation (requires GPU).""" if Version(transformers.__version__) < Version("4.57.0"): pytest.xfail("Bug in transformers solved in GH#40692, released in 4.57.0.") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage use_transformers_paged=True, # Enable transformers paged implementation report_to="none", logging_strategy="no", ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32") trainer = GRPOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=self.train_dataset, ) previous_trainable_params = {n: param.clone() for n, param in model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." release_memory(model, trainer) @pytest.mark.parametrize( "model_name", [ "HuggingFaceTB/SmolVLM-Instruct", # Only test the smaller model to avoid OOM ], ) @require_kernels @require_ampere_or_newer # Flash attention 2 requires Ampere or newer GPUs @require_bitsandbytes @require_peft def test_vlm_training(self, model_name): """ Test VLM training with aggressive memory optimization. This test uses multiple memory reduction techniques: - 4-bit quantization with double quantization - LoRA with very low rank (r=4) - Minimal batch size (1) with gradient accumulation - Small images (64x64 instead of 224x224) - Short sequences (max_completion_length=8) - Only 4 training samples - Only 1 training step - Gradient checkpointing and bfloat16 """ # Create processor once outside the data generator processor = AutoProcessor.from_pretrained(model_name, use_fast=True, padding_side="left") conversation = [ { "role": "user", "content": [ {"type": "image"}, {"type": "text", "text": "What is in the image?"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) def data_gen(num_samples): for _ in range(num_samples): yield { "prompt": prompt, "image": np.random.uniform(low=0.0, high=255.0, size=(64, 64, 3)).astype( np.uint8 ), # Much smaller images } dataset = Dataset.from_generator( data_gen, gen_kwargs={"num_samples": 4}, features=Features(image=Image(), prompt=Value(dtype="string")) ) # reduce memory requirements as much as possible quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype="bfloat16", bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_quant_storage="bfloat16", ) model = AutoModelForImageTextToText.from_pretrained( model_name, attn_implementation="kernels-community/flash-attn2", dtype="float32", device_map=get_kbit_device_map(), quantization_config=quantization_config, ) def reward_func(prompts, completions, **kwargs): # simple nonsensical reward return [-((len(c) - 25) ** 2) + 100 for c in completions] training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=1, # Minimal batch size gradient_accumulation_steps=2, # Maintain effective batch size num_generations=2, max_completion_length=8, # Much shorter completions bf16=True, # Use bfloat16 precision max_steps=1, # Only do 1 training step to save time and memory report_to="none", logging_strategy="no", ) lora_config = LoraConfig( task_type="CAUSAL_LM", r=4, # Much lower rank for minimal memory lora_alpha=8, # Reduced alpha proportionally lora_dropout=0.1, target_modules=["q_proj", "v_proj"], # Minimal target modules # For VLM models, we typically want to freeze the vision encoder # and only adapt the language model parameters modules_to_save=None, ) try: trainer = GRPOTrainer( model=model, processing_class=processor, reward_funcs=[reward_func], args=training_args, train_dataset=dataset, peft_config=lora_config, ) assert isinstance(trainer.model, PeftModel) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that LoRA parameters have changed # For VLM models, we're more permissive about which parameters can change lora_params_changed = False for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if "lora" in n.lower(): # LoRA parameters should change if not torch.equal(param, new_param): lora_params_changed = True # At least some LoRA parameters should have changed during training assert lora_params_changed, "No LoRA parameters were updated during training." except torch.OutOfMemoryError as e: pytest.skip(f"Skipping VLM training test due to insufficient GPU memory: {e}") except Exception as e: # Check for other memory-related errors if any(keyword in str(e).lower() for keyword in ["memory", "cuda", "out of memory", "insufficient"]): pytest.skip(f"Skipping VLM training test due to hardware constraints: {e}") else: raise release_memory(model, trainer) @require_vllm @require_bitsandbytes @require_peft def test_vlm_processor_vllm_colocate_mode(self): """ Test that VLM processors work with vLLM in colocate mode. This test uses multiple memory optimization techniques to ensure it runs on limited hardware: - LoRA (Low-Rank Adaptation) with minimal rank (r=4) - 4-bit quantization with BitsAndBytesConfig - Gradient checkpointing - bfloat16 precision - Minimal batch sizes and sequence lengths - Very low GPU memory utilization (5%) """ dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") config = GRPOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=1, # Minimal batch size gradient_accumulation_steps=2, # Make effective batch size 2, divisible by num_generations num_generations=2, max_completion_length=4, # Very short completions to reduce memory use_vllm=True, # Enable vLLM vllm_mode="colocate", # Use colocate mode to avoid server dependency vllm_gpu_memory_utilization=0.05, # Use minimal GPU memory (5%) bf16=True, # Use bfloat16 to reduce memory report_to="none", logging_strategy="no", ) # Create a VLM processor processor = AutoProcessor.from_pretrained("HuggingFaceTB/SmolVLM-Instruct", use_fast=True, padding_side="left") # Verify processor has both required attributes for VLM detection assert hasattr(processor, "tokenizer") assert hasattr(processor, "image_processor") def dummy_reward_func(completions, **kwargs): return [1.0] * len(completions) # Use LoRA configuration for memory efficiency lora_config = LoraConfig( r=4, # Very low rank for minimal memory lora_alpha=8, target_modules=["q_proj", "v_proj"], # Minimal target modules lora_dropout=0.1, bias="none", task_type="CAUSAL_LM", ) # Use 4-bit quantization for further memory reduction quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, ) original_env = {} required_env_vars = { "RANK": "0", "LOCAL_RANK": "0", "WORLD_SIZE": "1", "LOCAL_WORLD_SIZE": "1", "MASTER_ADDR": "localhost", "MASTER_PORT": "12355", } for key, value in required_env_vars.items(): original_env[key] = os.environ.get(key) os.environ[key] = value try: # Test VLM processor with vLLM colocate mode with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") try: # Load model with quantization for memory efficiency model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", quantization_config=quantization_config, dtype=torch.bfloat16, ) trainer = GRPOTrainer( model=model, reward_funcs=dummy_reward_func, args=config, train_dataset=dataset, processing_class=processor, # VLM processor peft_config=lora_config, # Use LoRA for memory efficiency ) # Should detect VLM processor correctly and allow vLLM assert trainer.use_vllm, "vLLM should be enabled for VLM processors in colocate mode" assert trainer.vllm_mode == "colocate", "Should use colocate mode" # Check if signature columns were set properly if trainer._signature_columns is not None: # Should include 'image' in signature columns for VLM processors assert "image" in trainer._signature_columns, ( "Should include 'image' in signature columns for VLM" ) # Should not emit any warnings about VLM incompatibility incompatibility_warnings = [ str(w_item.message) for w_item in w if "does not support VLMs" in str(w_item.message) or "not compatible" in str(w_item.message).lower() ] assert len(incompatibility_warnings) == 0, ( f"Should not emit VLM incompatibility warnings, but got: {incompatibility_warnings}" ) # Test passes if we get this far without exceptions except Exception as e: # If vLLM fails to initialize due to hardware constraints or other issues, that's expected if any( keyword in str(e).lower() for keyword in [ "outofmemoryerror", "cuda", "memory", "insufficient", "no such device", "free memory", "gpu memory utilization", "decrease gpu memory", ] ): pytest.skip(f"Skipping vLLM colocate test due to hardware constraints: {e}") elif "KeyError" in str(e) and "RANK" in str(e): pytest.skip(f"Skipping vLLM colocate test due to environment setup issues: {e}") elif "ValueError" in str(e) and "memory" in str(e).lower(): pytest.skip(f"Skipping vLLM colocate test due to memory constraints: {e}") else: raise finally: # Restore original environment variables for key, original_value in original_env.items(): if original_value is None: os.environ.pop(key, None) else: os.environ[key] = original_value release_memory(model, trainer) @require_vllm def test_training_vllm(self): """Test that training works with vLLM for generation.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = GRPOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", logging_strategy="no", use_vllm=True, ) try: trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny models are too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." except Exception as e: # If vLLM fails to initialize due to hardware constraints or other issues, that's expected if any( keyword in str(e).lower() for keyword in [ "outofmemoryerror", "cuda", "memory", "insufficient", "no such device", "free memory", "gpu memory utilization", "decrease gpu memory", ] ): pytest.skip(f"Skipping vLLM training test due to hardware constraints: {e}") elif "KeyError" in str(e) and "RANK" in str(e): pytest.skip(f"Skipping vLLM training test due to environment setup issues: {e}") elif "ValueError" in str(e) and "memory" in str(e).lower(): pytest.skip(f"Skipping vLLM training test due to memory constraints: {e}") else: raise release_memory(trainer.model, trainer) ================================================ FILE: tests/test_model_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from transformers import AutoModelForCausalLM from trl.models.utils import disable_gradient_checkpointing class TestDisableGradientCheckpointing: def test_when_disabled(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") assert model.is_gradient_checkpointing is False with disable_gradient_checkpointing(model): assert model.is_gradient_checkpointing is False assert model.is_gradient_checkpointing is False def test_when_enabled(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") model.gradient_checkpointing_enable() assert model.is_gradient_checkpointing is True with disable_gradient_checkpointing(model): assert model.is_gradient_checkpointing is False assert model.is_gradient_checkpointing is True ================================================ FILE: tests/test_reward_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import pathlib import pytest import torch from datasets import load_dataset from transformers import AutoModelForSequenceClassification, AutoTokenizer from transformers.utils import is_peft_available from trl import RewardConfig, RewardTrainer from trl.trainer.reward_trainer import DataCollatorForPreference from .testing_utils import TrlTestCase, require_peft if is_peft_available(): from peft import LoraConfig, get_peft_model class TestDataCollatorForPreference(TrlTestCase): def test_basic_padding(self): """Test basic padding functionality without completion masks.""" collator = DataCollatorForPreference(pad_token_id=0) examples = [ {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5]}, {"chosen_ids": [6, 7], "rejected_ids": [8]}, ] result = collator(examples) torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [6, 7, 0], [4, 5, 0], [8, 0, 0]])) torch.testing.assert_close( result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0], [1, 1, 0], [1, 0, 0]]) ) def test_pad_to_multiple_of(self): """Test padding to multiple of specified value.""" collator = DataCollatorForPreference(pad_token_id=0, pad_to_multiple_of=4) examples = [ {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5]}, {"chosen_ids": [6, 7], "rejected_ids": [8]}, ] result = collator(examples) torch.testing.assert_close( result["input_ids"], torch.tensor([[1, 2, 3, 0], [6, 7, 0, 0], [4, 5, 0, 0], [8, 0, 0, 0]]) ) torch.testing.assert_close( result["attention_mask"], torch.tensor([[1, 1, 1, 0], [1, 1, 0, 0], [1, 1, 0, 0], [1, 0, 0, 0]]) ) def test_single_example(self): """Test collator with a single example.""" collator = DataCollatorForPreference(pad_token_id=0) examples = [{"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5]}] result = collator(examples) torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) def test_different_pad_token_id(self): """Test with different pad token ID.""" collator = DataCollatorForPreference(pad_token_id=999) examples = [ {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5]}, {"chosen_ids": [6, 7], "rejected_ids": [8]}, ] result = collator(examples) torch.testing.assert_close( result["input_ids"], torch.tensor([[1, 2, 3], [6, 7, 999], [4, 5, 999], [8, 999, 999]]) ) torch.testing.assert_close( result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0], [1, 1, 0], [1, 0, 0]]) ) def test_collate_with_margin(self): collator = DataCollatorForPreference(pad_token_id=0) examples = [ {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5], "margin": 0.1}, {"chosen_ids": [6, 7], "rejected_ids": [8], "margin": 0.2}, ] result = collator(examples) torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [6, 7, 0], [4, 5, 0], [8, 0, 0]])) torch.testing.assert_close( result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0], [1, 1, 0], [1, 0, 0]]) ) torch.testing.assert_close(result["margin"], torch.tensor([0.1, 0.2])) class TestRewardTrainer(TrlTestCase): def test_raises_error_when_model_num_labels_not_one(self): """Test that RewardTrainer raises ValueError when model doesn't have num_labels=1.""" model = AutoModelForSequenceClassification.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32", # num_labels=2, # Defaults to 2 num_labels for causal models ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") with pytest.raises(ValueError, match=r"reward models require `num_labels=1`"): RewardTrainer(model=model, args=training_args, train_dataset=dataset) @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "trl-internal-testing/tiny-Qwen3MoeForCausalLM", "trl-internal-testing/tiny-LlamaForCausalLM-3.2", ], ) def test_train(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer(model=model_id, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize( "config_name", [ "standard_preference", "conversational_preference", "standard_implicit_prompt_preference", "conversational_implicit_prompt_preference", ], ) def test_train_dataset_types(self, config_name): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", config_name, split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_model(self): # Instantiate the model model = AutoModelForSequenceClassification.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", num_labels=1, # required for reward models dtype="float32", ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_from_sequence_classification_model(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_model_dtype(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig( output_dir=self.tmp_dir, model_init_kwargs={"dtype": torch.float16}, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): # For some reasonn model.layers.0.input_layernorm.weight doesn't change in GitHub Actions but does # locally. We ignore this parameter for now if "layernorm" in n: continue new_param = trainer.model.get_parameter(n) # Check the torch dtype assert new_param.dtype == torch.float16 assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_dense_with_peft_config(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForSequenceClassification.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_moe_with_peft_config(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen3MoeForCausalLM" model = AutoModelForSequenceClassification.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(target_modules=["up_proj", "down_proj", "score"]), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_peft_model(self): # Get the base model model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForSequenceClassification.from_pretrained( model_id, num_labels=1, # required for reward models dtype="float32", ) # Get the base model parameter names base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Turn the model into a peft model lora_config = LoraConfig() model = get_peft_model(model, lora_config) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # In practice, this test is the same as `test_train_dense_with_peft_config`, since gradient checkpointing is # enabled by default in `RewardTrainer`. We keep it as a regression guard: if the default ever changes, we still # explicitly test PEFT + gradient checkpointing, which has caused issues in the past. @require_peft def test_train_with_peft_config_and_gradient_checkpointing(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForSequenceClassification.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, gradient_checkpointing=True, report_to="none") trainer = RewardTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize("use_reentrant", [True, False]) @require_peft def test_train_with_peft_config_and_gradient_checkpointing_reentrant(self, use_reentrant): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" model = AutoModelForSequenceClassification.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig( output_dir=self.tmp_dir, gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": use_reentrant}, report_to="none", ) trainer = RewardTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize( "chosen_column,rejected_column,expect_deprecation_warning", [ ("chosen_ids", "rejected_ids", False), ("chosen_input_ids", "rejected_input_ids", True), ], ) def test_train_with_pretokenized_data(self, chosen_column, rejected_column, expect_deprecation_warning): # Get the dataset model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") def tokenize_example(example): return { chosen_column: tokenizer(example["chosen"]).input_ids, rejected_column: tokenizer(example["rejected"]).input_ids, } # Apply tokenization tokenized_dataset = dataset.map(tokenize_example, remove_columns=["chosen", "rejected"]) # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") if expect_deprecation_warning: with pytest.warns(FutureWarning, match=r"will not be supported in v1"): trainer = RewardTrainer(model=model_id, args=training_args, train_dataset=tokenized_dataset) else: trainer = RewardTrainer(model=model_id, args=training_args, train_dataset=tokenized_dataset) assert "chosen_ids" in trainer.train_dataset.column_names assert "rejected_ids" in trainer.train_dataset.column_names assert "chosen_input_ids" not in trainer.train_dataset.column_names assert "rejected_input_ids" not in trainer.train_dataset.column_names # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_iterable_dataset(self): # Get the dataset dataset = load_dataset( "trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train", streaming=True ) # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, max_steps=3, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_chat_template_kwargs(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") # The following template is a simplified version of the Qwen chat template, where an additional argument # `role_capital` is used to control the capitalization of roles. tokenizer.chat_template = '{%- if messages[0]["role"] == "system" -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\n" + messages[0]["content"] + "<|im_end|>\\n" }}{%- else -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n" }}{%- endif -%}{%- for message in messages -%} {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) -%} {{ "<|im_start|>" + (message.role.upper() if role_capital else message.role) + "\\n" + message.content + "<|im_end|>\\n" }} {%- elif message.role == "assistant" -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") }} {%- if message.content -%} {{ "\\n" + message.content }} {%- endif -%} {{ "<|im_end|>\\n" }} {%- elif message.role == "tool" -%} {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") -%} {{ "<|im_start|>" + ("USER" if role_capital else "user") }} {%- endif -%} {{ "\\n\\n" + message.content + "\\n" }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") -%} {{ "<|im_end|>\\n" }} {%- endif -%} {%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") + "\\n" }}{%- endif -%}' dataset = dataset.add_column( "chat_template_kwargs", [{"role_capital": bool(i % 2)} for i in range(len(dataset))] ) assert "chat_template_kwargs" in dataset.features trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, processing_class=tokenizer, ) # Assert trainer uses the same chat template as tokenizer assert trainer.processing_class.chat_template == tokenizer.chat_template # Assert chat_template is applied for i in range(2): role = "SYSTEM" if i else "system" system_prompt = ( f"<|im_start|>{role}\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>" ) system_prompt_ids = trainer.processing_class(system_prompt)["input_ids"] assert trainer.train_dataset[i]["chosen_ids"][: len(system_prompt_ids)] == system_prompt_ids assert trainer.train_dataset[i]["rejected_ids"][: len(system_prompt_ids)] == system_prompt_ids # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_set_chat_template_from_model(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, chat_template_path="Qwen/Qwen3-4B", report_to="none") # trl-internal-testing/tiny-GPTNeoXForCausalLM doesn't have a chat template set by default trainer = RewardTrainer( model="trl-internal-testing/tiny-GPTNeoXForCausalLM", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # RewardTrainer uses a mean-free loss that cancels uniform shifts in output scores. Since GPT-NeoX models # include a final LayerNorm, its bias consistently receives zero gradient and remains unchanged, so we skip # this parameter. if n == "gpt_neox.final_layer_norm.bias": continue assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_set_chat_template_from_path(self, lazy_shared_datadir): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_preference", split="train") # Initialize the trainer training_args = RewardConfig( output_dir=self.tmp_dir, chat_template_path=str(lazy_shared_datadir / "template.jinja"), report_to="none", ) # trl-internal-testing/tiny-GPTNeoXForCausalLM doesn't have a chat template set by default trainer = RewardTrainer( model="trl-internal-testing/tiny-GPTNeoXForCausalLM", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # RewardTrainer uses a mean-free loss that cancels uniform shifts in output scores. Since GPT-NeoX models # include a final LayerNorm, its bias consistently receives zero gradient and remains unchanged, so we skip # this parameter. if n == "gpt_neox.final_layer_norm.bias": continue assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # Check that the template saved in the output directory is the same as the one used for training template_path = pathlib.Path(self.tmp_dir) / "checkpoint-9" / "chat_template.jinja" assert template_path.exists(), f"Chat template not found at {template_path}" with open(template_path) as f: template_content = f.read() with open(training_args.chat_template_path) as f: original_template_content = f.read() assert template_content == original_template_content, "Chat template content does not match the original" def test_train_toolcall_data(self): # Get the dataset dataset = load_dataset("trl-internal-testing/toolcall", "preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_toolcall_data_as_json(self): # Tabular backends (Arrow/Parquet) can insert `None` for missing keys in nested structures. # If `tools` is stored as a list of dicts and examples use different dict schemas, nulls may # be introduced and break tool processing. This test ensures we also support `tools` provided # as a list of dicts. dataset = load_dataset("trl-internal-testing/toolcall", "preference", split="train") def convert_to_json(example): return {"tools": json.loads(example["tools"])} dataset = dataset.map(convert_to_json) # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_eval(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Train the model trainer.train() # Check that the eval loss is not None assert trainer.state.log_history[0]["eval_loss"] is not None def test_train_with_multiple_eval_dataset(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset={"data1": dataset["test"], "data2": dataset["test"]}, ) # Train the model trainer.train() # Check that the eval losses are not None assert trainer.state.log_history[-3]["eval_data1_loss"] is not None assert trainer.state.log_history[-2]["eval_data2_loss"] is not None def test_train_with_compute_metrics(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference") def dummy_compute_metrics(eval_pred): return {"my_metric": 0.123} # Initialize the trainer training_args = RewardConfig( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none", ) trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], compute_metrics=dummy_compute_metrics, ) # Train the model trainer.train() # Check that the custom metric is logged assert trainer.state.log_history[-2]["eval_my_metric"] == 0.123 # In practice, this test is the same as `test_train`, since gradient checkpointing is enabled by default in # `RewardTrainer`. We keep it as a regression guard: if the default ever changes, we still explicitly test gradient # checkpointing, which has caused issues in the past. def test_train_with_gradient_checkpointing(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, gradient_checkpointing=True, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize("use_reentrant", [True, False]) def test_train_with_gradient_checkpointing_reentrant(self, use_reentrant): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig( output_dir=self.tmp_dir, gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": use_reentrant}, report_to="none", ) trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_tag_added(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, ) for tag in ["reward-trainer", "trl"]: assert tag in trainer.model.model_tags @require_peft def test_tag_added_peft(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, peft_config=LoraConfig(), ) for tag in ["reward-trainer", "trl"]: assert tag in trainer.model.model_tags def test_train_with_margin(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") def add_margin(example): # dummy margin based on the length of the chosen summary return {"margin": len(example["chosen"])} dataset = dataset.map(add_margin) # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_center_rewards_coefficient(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = RewardConfig(output_dir=self.tmp_dir, center_rewards_coefficient=0.01, report_to="none") trainer = RewardTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" ================================================ FILE: tests/test_rewards.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import threading from trl.rewards import accuracy_reward, get_soft_overlong_punishment, reasoning_accuracy_reward, think_format_reward from .testing_utils import TrlTestCase, require_math_latex class TestThinkFormatReward(TrlTestCase): def test_valid_format(self): completions = [ "This is my reasoning.This is my answer.", # Simple, one-line reasoning "\nThis is my reasoning.\n\nThis is my answer.", # Multiline reasoning "\nThis is\nmy reasoning.\n\nThis is my answer.", # Multiline reasoning "\nThis is my reasoning.\nThis is my answer.", # Reasoning including other tags "\nThis is my answer.", # Empty reasoning ] completions = [[{"content": completion}] for completion in completions] expected_rewards = [1.0, 1.0, 1.0, 1.0, 1.0] # All should be valid rewards = think_format_reward(completions) assert rewards == expected_rewards def test_invalid_format(self): completions = [ "\nThis is my reasoning.\nThis is my answer.", # No closing "This is my reasoning.\nThis is my answer.", # No closing "This is my reasoning. This is my answer.", # No tags "This is my reasoning.\nThis is my answer.", # No tags "This is my reasoning.\nThis is my answer.", # No opening "This is my reasoning.This is my answer.", # No opening "Thisis my reasoning.\nThis is my answer.", # tag in the middle "This ismy reasoning.This is my answer.", # Nested tags "This is\nmy\nreasoning.\nThis is my answer.", # Multiline ] completions = [[{"content": completion}] for completion in completions] expected_rewards = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] # All should be invalid rewards = think_format_reward(completions) assert rewards == expected_rewards def test_mixed_format(self): completions = [ "This is my reasoning.This is my answer.", # Valid "\nThis is my reasoning.\n\nThis is my answer.", # Valid "This is my reasoning.\nThis is my answer.", # Invalid "This is my reasoning. This is my answer.", # Invalid ] completions = [[{"content": completion}] for completion in completions] expected_rewards = [1.0, 1.0, 0.0, 0.0] rewards = think_format_reward(completions) assert rewards == expected_rewards class TestSoftOverlongPunishmentReward: def test_soft_overlong_punishment_short_completion(self): """Test soft overlong punishment reward function with a short completion.""" # length 50, with max=100 and soft cache=20, reward should be 0. reward_fn = get_soft_overlong_punishment(max_completion_len=100, soft_punish_cache=20) completion_ids = [[1] * 50] # 50 <= 80 rewards = reward_fn(completion_ids=completion_ids) assert rewards == [0] def test_soft_overlong_punishment_long_completion(self): """Test soft overlong punishment reward function with a longer than max completion.""" # 110 > 100, reward should be -1. reward_fn = get_soft_overlong_punishment(max_completion_len=100, soft_punish_cache=20) completion_ids = [[1] * 110] rewards = reward_fn(completion_ids) assert rewards == [-1] def test_soft_overlong_punishment_intermediate_completion(self): """Test soft overlong punishment reward function for intermediate length completion.""" reward_fn = get_soft_overlong_punishment(max_completion_len=100, soft_punish_cache=20) completion_ids = [[1] * 90] # 90 is between 80 and 100 rewards = reward_fn(completion_ids) assert round(abs(rewards[0] - -0.5), 4) == 0 class TestAccuracyReward: @require_math_latex def test_accuracy_reward_correct_answer(self): """Test accuracy_reward with a correct answer.""" completion = [[{"content": r"\boxed{\frac{63}{400}}"}], [{"content": r"\boxed{\frac{63}{400}}"}]] solution = [r"\frac{63}{400}", "63/400"] rewards = accuracy_reward(completion, solution) assert rewards[0] == 1.0 assert rewards[1] == 1.0 @require_math_latex def test_accuracy_reward_wrong_answer(self): """Test accuracy_reward with an incorrect answer.""" completion = [[{"content": r"\boxed{\frac{64}{400}}"}]] solution = [r"\frac{63}{400}"] rewards = accuracy_reward(completion, solution) assert rewards[0] == 0.0 @require_math_latex def test_accuracy_reward_wrong_answer_no_latex(self): """Test accuracy_reward with an incorrect answer and gold solution with no latex.""" completion = [[{"content": r"\boxed{3}"}]] solution = ["6"] rewards = accuracy_reward(completion, solution) assert rewards[0] == 0.0 @require_math_latex def test_accuracy_reward_unparsable_gold(self): """Test accuracy_reward with an unparsable gold solution.""" completion = [ [{"content": "Answer is forty two."}], [{"content": r"Some other content. \boxed{43}."}], ] solution = [ "Answer is forty two.", "Answer is forty three.", ] rewards = accuracy_reward(completion, solution) assert rewards[0] is None assert rewards[1] is None @require_math_latex def test_accuracy_reward_in_worker_thread(self): """Test that accuracy_reward works when called from a non-main thread.""" completions = [[{"content": r"\boxed{\frac{1}{3}}"}]] solutions = [r"\frac{1}{3}"] results = [] exceptions = [] def target(): try: results.extend(accuracy_reward(completions, solutions)) except Exception as e: exceptions.append(e) t = threading.Thread(target=target) t.start() t.join() assert not exceptions, f"accuracy_reward raised in worker thread: {exceptions[0]}" assert results == [1.0] class TestReasoningAccuracyReward: @require_math_latex def test_correct_answer_yields_unit_reward(self): completions = [ [{"content": r" Reasoning content \boxed{\frac{63}{400}}"}], [{"content": r"Reasoning content \boxed{\frac{63}{400}}"}], ] solutions = [r"\frac{63}{400}", r"\frac{63}{400}"] rewards = reasoning_accuracy_reward(completions, solutions) assert rewards[0] == 1.0 assert rewards[1] == 1.0 @require_math_latex def test_correct_answer_with_custom_tags_yields_unit_reward(self): completions = [ [{"content": r" Reasoning content \boxed{\frac{63}{400}}"}], ] solutions = [ r"\frac{63}{400}", ] rewards = reasoning_accuracy_reward(completions, solutions, reasoning_delimiters=[""]) assert rewards[0] == 1.0 @require_math_latex def test_incorrect_answer_yields_zero_reward(self): completion = [[{"content": r" Reasoning content \boxed{\frac{64}{400}}"}]] solution = [r"\frac{63}{400}"] rewards = reasoning_accuracy_reward(completion, solution) assert rewards[0] == 0.0 @require_math_latex def test_correct_answer_in_reasoning_yields_zero_reward(self): completions = [ [{"content": r" My answer is \boxed{42} Some other text."}], [{"content": r" The answer is \boxed{42} Here's a wrong answer: \boxed{43}."}], ] solutions = [r"\boxed{42}", r"\boxed{42}"] rewards = reasoning_accuracy_reward(completions, solutions) assert rewards[0] == 0.0 assert rewards[1] == 0.0 @require_math_latex def test_incomplete_reasoning_yields_zero_reward(self): completions = [ [{"content": r" Incomplete reasoning without closing tag"}], [{"content": r"Correct answer \frac{63}{400} but completely missing reasoning content"}], ] solutions = [r"\frac{63}{400}", r"\frac{63}{400}"] rewards = reasoning_accuracy_reward(completions, solutions) assert rewards[0] == 0.0 assert rewards[1] == 0.0 @require_math_latex def test_unparsable_gold_solution_yields_none_reward(self): completions = [ [{"content": r" Reasoning content \boxed{42}"}], ] solutions = [ "forty two", ] rewards = reasoning_accuracy_reward(completions, solutions) assert rewards[0] is None ================================================ FILE: tests/test_rich_progress_callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch import torch.nn as nn from datasets import Dataset from transformers import Trainer, TrainingArguments from trl.trainer.callbacks import RichProgressCallback from .testing_utils import TrlTestCase, require_rich class DummyModel(nn.Module): def __init__(self): super().__init__() self.a = nn.Parameter(torch.tensor(1.0)) def forward(self, x): return self.a * x @require_rich class TestRichProgressCallback(TrlTestCase): def setup_method(self): self.dummy_model = DummyModel() self.dummy_train_dataset = Dataset.from_list([{"x": 1.0, "y": 2.0}] * 5) self.dummy_val_dataset = Dataset.from_list([{"x": 1.0, "y": 2.0}] * 101) def test_rich_progress_callback_logging(self): training_args = TrainingArguments( output_dir=self.tmp_dir, per_device_eval_batch_size=2, per_device_train_batch_size=2, num_train_epochs=4, eval_strategy="steps", eval_steps=1, logging_strategy="steps", logging_steps=1, save_strategy="no", report_to="none", disable_tqdm=True, ) callbacks = [RichProgressCallback()] trainer = Trainer( model=self.dummy_model, train_dataset=self.dummy_train_dataset, eval_dataset=self.dummy_val_dataset, args=training_args, callbacks=callbacks, ) trainer.train() ================================================ FILE: tests/test_rloo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest.mock import patch import pytest import torch import transformers from datasets import load_dataset from packaging.version import Version from transformers import ( AutoModelForCausalLM, AutoModelForImageTextToText, AutoModelForSequenceClassification, AutoTokenizer, ) from transformers.utils import is_peft_available from trl import RLOOConfig, RLOOTrainer from .testing_utils import TrlTestCase, require_peft, require_vision, require_vllm if is_peft_available(): from peft import LoraConfig, get_peft_model class TestRLOOTrainer(TrlTestCase): def test_init_minimal(self): # Test that RLOOTrainer can be instantiated with only model, reward_model and train_dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", train_dataset=dataset, ) @pytest.mark.parametrize("config_name", ["standard_prompt_only", "conversational_prompt_only"]) def test_training(self, config_name): dataset = load_dataset("trl-internal-testing/zen", config_name, split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_eval(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") training_args = RLOOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage per_device_eval_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage eval_strategy="steps", eval_steps=2, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() def test_training_with_num_generations_eval(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only") training_args = RLOOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage per_device_eval_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_generations_eval=1, eval_strategy="steps", eval_steps=2, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) trainer.train() def test_training_multiple_iterations(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_iterations=2, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_peft def test_training_peft_config(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @require_peft def test_training_peft_model(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] lora_config = LoraConfig() model = get_peft_model(model, lora_config) dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n and "ref" not in n: # and the peft params to be different (except base and ref) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." # In practice, this test is the same as `test_training_peft_config`, since gradient checkpointing is enabled by # default in `RLOOTrainer`. We keep it as a regression guard: if the default ever changes, we still explicitly test # PEFT + gradient checkpointing, which has caused issues in the past. @require_peft def test_training_peft_with_gradient_checkpointing(self): model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage gradient_checkpointing=True, # enable gradient checkpointing report_to="none", ) trainer = RLOOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." def test_training_different_reward_model(self): # Use a reward model different from the model: different chat template, tokenization, etc. dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") reward_model_id = "trl-internal-testing/tiny-LlamaForSequenceClassification-3.2" reward_model = AutoModelForSequenceClassification.from_pretrained(reward_model_id) reward_tokenizer = AutoTokenizer.from_pretrained(reward_model_id) # By default, the trainer uses the eos token as the padding token. However, for Llama models, the eos token # appears in the chat template. Using it as a pad token disrupts the reward calculation, as the calculation # considers the score of the last token before the first pad token. To ensure correct reward calculations, # we use a separate pad token instead. reward_tokenizer.pad_token = "<|finetune_right_pad_id|>" training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_model, args=training_args, train_dataset=dataset, reward_processing_classes=reward_tokenizer, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_standard(self): # Test if trainer can handle reward function with standard format dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_conversational(self): # Test if trainer can handle reward function with conversational format dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that gives higher scores to longer completion content.""" completion_contents = [completion[0]["content"] for completion in completions] return [float(len(content)) for content in completion_contents] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs(self): # Test that RLOOTrainer can be instantiated with multiple reward functions dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def reward_func2(completions, **kwargs): """Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func1, reward_func2], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_sync_and_async_reward_funcs(self): # Test that RLOOTrainer can be instantiated with multiple reward functions one of which is async dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def sync_reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def sync_reward_func2(completions, **kwargs): return [1 for _ in completions] async def async_reward_func(completions, **kwargs): """Async Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[sync_reward_func1, sync_reward_func2, async_reward_func], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs_with_None_output(self): """Test that a valid math reward function is processed correctly while the code reward function returns None.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def applicable_reward_func(completions, **kwargs): """A reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def non_applicable_reward_func(completions, **kwargs): """A reward function that returns None for all inputs, as it is not applicable to this sample.""" return [None] * len(completions) training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[ applicable_reward_func, non_applicable_reward_func, ], # One applicable, one non applicable args=training_args, train_dataset=dataset, ) previous_trainable_params = { n: param.clone() for n, param in trainer.model.named_parameters() if param.requires_grad } trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_reward_funcs_with_weights(self): """Test that RLOOTrainer can handle multiple reward functions with weights.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func1(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] def reward_func2(completions, **kwargs): """Reward function that rewards completions with more unique letters.""" return [float(len(set(completion))) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", reward_weights=[0.7, 0.3], # weight of reward_func1 and reward_func2 respectively ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func1, reward_func2], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training logs contain both reward metrics assert trainer.state.log_history[-1]["train_loss"] is not None assert "rewards/reward_func1/mean" in trainer.state.log_history[-1] assert "rewards/reward_func1/std" in trainer.state.log_history[-1] assert "rewards/reward_func2/mean" in trainer.state.log_history[-1] assert "rewards/reward_func2/std" in trainer.state.log_history[-1] # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_mixed_reward_funcs(self): # Test if the trainer can handle a mix of reward functions and reward models dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion)) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=[reward_func, "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5"], args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_reward_func_additional_column(self): # Test if trainer can handle reward function that rely on additional columns in the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Add a column to the dataset (dummy example, the column could be anything) some_values = list(range(len(dataset))) dataset = dataset.add_column("some_values", some_values) def reward_func(completions, some_values, **kwargs): """Reward function that rewards completions with lengths closer to the values in some_values.""" return [ float(abs(len(completion) - value)) for completion, value in zip(completions, some_values, strict=True) ] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_sync_ref_model(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, beta=0.1, # ensure ref model is created so sync can update it learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage sync_ref_model=True, ref_model_sync_steps=2, # reduce sync steps to ensure a sync happens report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} assert trainer.ref_model is not None previous_ref_params = {n: param.clone() for n, param in trainer.ref_model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." new_ref_param = trainer.ref_model.get_parameter(n) assert not torch.equal(previous_ref_params[n], new_ref_param), f"Ref Parameter {n} has not changed." def test_training_beta_zero(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, beta=0.0, # set beta to zero value to test the case where the reference model is not used learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_pad_to_multiple_of(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage pad_to_multiple_of=8, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_peft @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_and_peft(self): """Test that training works with vLLM for generation.""" model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-0.5B-Instruct", dtype="float32" ) # tiny model is too small for vLLM base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, ) lora_config = LoraConfig( target_modules="all-linear", # test with non-default modules as it adds extra keys in state_dict that we need to handle modules_to_save=["embed_tokens", "lm_head"], ) trainer = RLOOTrainer( model=model, reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, peft_config=lora_config, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n and "original_module" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_structured_outputs(self): """Test that training works with vLLM for generation with structured outputs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, vllm_structured_outputs_regex=r"\n.*\n\n\n.*\n", ) trainer = RLOOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny model is too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_additional_generation_kwargs(self): """Test that training works with additional generation kwargs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", top_p=0.9, top_k=10, min_p=0.01, repetition_penalty=1.1, ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vllm_with_additional_generation_kwargs(self): """Test that training works with vLLM and additional generation kwargs.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", use_vllm=True, top_p=0.9, top_k=10, min_p=0.01, repetition_penalty=1.1, ) trainer = RLOOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", # tiny model is too small for vLLM reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_normalized_advantages(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage normalize_advantages=True, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_clipped_rewards(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage reward_clip_range=(-1, 1), report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @patch("transformers.generation.utils.GenerationMixin.generate") def test_training_with_mask_truncated_completions(self, mock_generate): """Test that training works with mask_truncated_completions=True parameter.""" # We mock the generate method because the model's random weights make it extremely unlikely to produce a # sequence containing the EOS token within the allowed max_completion_length. As a result, all tokens are # masked in the loss, the model doesn't update, and the final check (which verifies the update) fails. def fake_generate(input_ids, **kwargs): # pad_token_id = 151643; eos_token_id = 151645 completion_ids = torch.tensor( [ [1, 2, 3, 4, 5, 6, 7, 8], # this one is truncated [9, 10, 11, 151645, 151643, 151643, 151643, 151643], # this one contains eos [12, 13, 14, 15, 16, 17, 18, 151645], # particular case, eos is generated just within the limit ], device=input_ids.device, ) return torch.cat([input_ids, completion_ids], dim=1) mock_generate.side_effect = fake_generate dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage mask_truncated_completions=True, # Enable masking of truncated completions report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_mask_truncated_completions_all_masked(self): """ Test that when all generated completions are truncated (i.e., none contain an EOS token), and mask_truncated_completions=True, the model receives no effective learning signal and therefore does not update its parameters. Here, we don't mock the generate method, be we rely on the fact that the model the probability of generating the EOS token is extremely low, so all generated completions are truncated. """ dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage mask_truncated_completions=True, # Enable masking of truncated completions report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert torch.equal(param, new_param), f"Parameter {n} has changed." def test_warning_raised_all_rewards_none(self, caplog): """Test that a proper warning is raised when all rewards are None.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def always_none_reward_func(completions, **kwargs): """Reward function that always returns None.""" return [None] * len(completions) training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=always_none_reward_func, args=training_args, train_dataset=dataset, ) with caplog.at_level("WARNING", logger="trl.trainer.rloo_trainer"): trainer.train() expected_warning = "All reward functions returned None for the following kwargs:" assert expected_warning in caplog.text def test_training_num_generations_larger_than_batch_size(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage num_generations=6, # the number of generations is larger than the batch size, but gradient_accumulation_steps=2, # gradient accumulation should allow that report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_multiple_dataloader_workers(self): # Pytest/CI often starts background threads before tests run. With Python 3.12, using the default "fork" start # method in a multi-threaded process emits a DeprecationWarning and may deadlock. # # We force "spawn" here to make multiprocessing safe under pytest when DataLoader workers are enabled. This is # test-environment–specific and not required by the training logic itself. # # This means the test does not cover "fork". However, "spawn" is stricter (requires full picklability and clean # state) and avoids fork-after-threads issues that pytest cannot reliably test anyway. Fork-specific behavior, # if needed, should be tested in a clean process outside pytest. torch.multiprocessing.set_start_method("spawn", force=True) dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage dataloader_num_workers=2, # use multiple dataloader workers report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_generation_kwargs(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage # Pass gen kwargs generation_kwargs={"do_sample": True, "top_k": 50, "num_beams": 2, "length_penalty": -0.1}, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_reward_func_accessing_trainer_state(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): trainer_state = kwargs.get("trainer_state") assert trainer_state is not None # transformers.TrainerState instance should have a `global_step` property. assert hasattr(trainer_state, "global_step") return [float(len(set(completion))) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() def test_training_reward_func_with_log_extra(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): log_extra = kwargs.get("log_extra") assert log_extra is not None log_extra("test_column", [completion[:5] for completion in completions]) return [float(len(completion)) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", log_completions=True, ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() assert "test_column" in trainer._logs["extra"] def test_training_reward_func_with_log_metric(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") def reward_func(completions, **kwargs): log_metric = kwargs.get("log_metric") assert log_metric is not None log_metric("custom_accuracy", 0.75) return [float(len(completion)) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) trainer.train() # log_metric appends to _metrics, which gets averaged and merged into log_history logged_keys = {k for entry in trainer.state.log_history for k in entry} assert "custom_accuracy" in logged_keys def test_prepare_input_called_with_correct_data(self): dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_completion_length=8, # reduce the completion length to reduce memory usage gradient_accumulation_steps=3, # can be anything in this test # steps_per_generation*per_device_train_batch_size=24 is divisible by num_generations=4 steps_per_generation=4, num_generations=4, per_device_train_batch_size=6, # reduce the batch size to reduce memory usage num_iterations=2, shuffle_dataset=False, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) # steps_per_generation=4, per_device_train_batch_size=6 and num_generations=4, so we expect a # generation batch of 24 samples (steps_per_generation * per_device_train_batch_size), containing 6 # different prompts (steps_per_generation * per_device_train_batch_size // num_generations), each repeated # 4 times (num_generations). expected_first_generation_batch = ( [{"prompt": "Beautiful is better than"}] * 4 + [{"prompt": "Explicit is"}] * 4 + [{"prompt": "Simple is better"}] * 4 + [{"prompt": "Complex"}] * 4 + [{"prompt": "Flat is better than"}] * 4 + [{"prompt": "Sparse is better"}] * 4 ) expected_second_generation_batch = ( [{"prompt": "Readability"}] * 4 + [{"prompt": "Special cases aren't special"}] * 4 + [{"prompt": "Although practicality beats"}] * 4 + [{"prompt": "Errors should never"}] * 4 + [{"prompt": "Unless explicitly"}] * 4 + [{"prompt": "In the face of ambiguity, refuse"}] * 4 ) with patch.object(RLOOTrainer, "training_step", wraps=trainer.training_step) as mock_prepare: trainer.train() # 3 epochs * 2 iterations * 2 generation batches to cover the dataset * 4 steps_per_generation assert mock_prepare.call_count == 48 for i in range(0, 8): # Generation batch repeated 8 times (steps_per_generation*num_iterations) assert mock_prepare.call_args_list[i].args[1] == expected_first_generation_batch for i in range(8, 16): assert mock_prepare.call_args_list[i].args[1] == expected_second_generation_batch @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", "trl-internal-testing/tiny-LlavaNextForConditionalGeneration", "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", "trl-internal-testing/tiny-Qwen2VLForConditionalGeneration", pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.2.0"), reason="Qwen3.5 models were introduced in transformers-5.2.0", ), ), # "trl-internal-testing/tiny-SmolVLMForConditionalGeneration", seems not to support bf16 properly ], ) @require_vision def test_training_vlm(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ( "model.vision_tower.", "model.multi_modal_projector.", "model.visual.", "model.image_newline", ) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @require_vision def test_training_vlm_with_pad_to_multiple_of(self): # Models like Gemma3 use other forward keyword arguments like token_type_ids that also need to be padded when # using pad_to_multiple_of, so we test that the trainer correctly pads all the necessary inputs in this case. dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage pad_to_multiple_of=7, report_to="none", ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Gemma3ForConditionalGeneration", reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision def test_training_vlm_beta_non_zero(self, model_id): dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, beta=0.1, # set beta to non-zero value to test the case where the reference model is used learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed # Because of the way the tiny models are initialized, the gradient does not flow properly through the # vision parts of the model, so we skip them. Ideally, we should fix the init of these models. params_to_skip = ("model.visual.",) for n, param in previous_trainable_params.items(): if n.startswith(params_to_skip): continue new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision @require_peft def test_training_vlm_peft(self, model_id): model = AutoModelForImageTextToText.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model, reward_funcs=reward_func, args=training_args, train_dataset=dataset, peft_config=LoraConfig(target_modules=["q_proj", "v_proj"]), ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model params to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed." elif "base_layer" not in n: # We expect the peft params to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", ], ) @require_vision @require_vllm @pytest.mark.skip(reason="We should add a mock for the vLLM server.") def test_training_vlm_and_vllm(self, model_id) -> None: dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", use_vllm=True, vllm_mode="server", ) trainer = RLOOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @require_vision def test_training_vlm_multi_image(self, model_id): dataset = load_dataset("trl-internal-testing/zen-multi-image", "conversational_prompt_only", split="train") def reward_func(completions, **kwargs): """Reward function that rewards longer completions.""" return [float(len(completion[0]["content"])) for completion in completions] training_args = RLOOConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, # reduce the batch size to reduce memory usage num_generations=3, # reduce the number of generations to reduce memory usage max_completion_length=8, # reduce the completion length to reduce memory usage report_to="none", ) trainer = RLOOTrainer( model=model_id, reward_funcs=reward_func, args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_training_with_chat_template_kwargs(self): dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_only", split="train") training_args = RLOOConfig( bf16=False, output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates per_device_train_batch_size=3, num_generations=3, max_completion_length=8, report_to="none", chat_template_kwargs={"enable_thinking": False}, ) trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", reward_funcs="trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", args=training_args, train_dataset=dataset, ) previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() assert trainer.state.log_history[-1]["train_loss"] is not None # Check that the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.equal(param, new_param), f"Parameter {n} has not changed." def test_mismatched_reward_processing_classes_length(self): """Test that mismatched length between reward_funcs and reward_processing_classes raises error.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use two reward models reward_models = [ "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", "trl-internal-testing/tiny-Qwen3ForSequenceClassification", ] # Create a single processing class (tokenizer) single_processing_class = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) training_args = RLOOConfig(output_dir=self.tmp_dir, report_to="none") with pytest.raises(ValueError, match="must match"): RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_models, reward_processing_classes=single_processing_class, # only one, but need two args=training_args, train_dataset=dataset, ) def test_correct_reward_processing_classes_list(self): """Test that correct list of reward_processing_classes works properly.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use two reward models reward_models = [ "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5", "trl-internal-testing/tiny-Qwen3ForSequenceClassification", ] # Create processing classes processing_class1 = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) processing_class2 = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen3ForSequenceClassification") training_args = RLOOConfig(output_dir=self.tmp_dir, report_to="none") # Correct list length should work correct_processing_classes = [processing_class1, processing_class2] trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_models, reward_processing_classes=correct_processing_classes, args=training_args, train_dataset=dataset, ) assert len(trainer.reward_processing_classes) == len(reward_models) def test_single_reward_model_with_single_processing_class(self): """Test that single reward model with single processing class works.""" dataset = load_dataset("trl-internal-testing/zen", "standard_prompt_only", split="train") # Use single reward model reward_model = "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" # Create a single processing class (tokenizer) single_processing_class = AutoTokenizer.from_pretrained( "trl-internal-testing/tiny-Qwen2ForSequenceClassification-2.5" ) training_args = RLOOConfig(output_dir=self.tmp_dir, report_to="none") trainer = RLOOTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", reward_funcs=reward_model, reward_processing_classes=single_processing_class, # single object for single reward model args=training_args, train_dataset=dataset, ) assert len(trainer.reward_processing_classes) == 1 assert trainer.reward_processing_classes[0] == single_processing_class ================================================ FILE: tests/test_sft_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gc import json import pathlib from unittest.mock import MagicMock, patch import pytest import torch import transformers from accelerate.utils.memory import release_memory from datasets import load_dataset from packaging.version import Version from packaging.version import parse as parse_version from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments from transformers.testing_utils import backend_empty_cache, torch_device from transformers.utils import is_peft_available from trl import SFTConfig, SFTTrainer from trl.trainer.sft_trainer import DataCollatorForLanguageModeling, dft_loss from .testing_utils import ( TrlTestCase, ignore_warnings, require_ampere_or_newer, require_bitsandbytes, require_kernels, require_liger_kernel, require_peft, require_torch_accelerator, require_torch_multi_accelerator, require_vision, ) if is_peft_available(): import peft from peft import ( LoraConfig, PeftModel, PrefixTuningConfig, PromptEncoderConfig, PromptTuningConfig, TaskType, get_peft_model, ) class TestDFTLoss(TrlTestCase): def test_dft_loss(self): batch_size = 2 seq_len = 3 vocab_size = 2 # All tokens have the same probability logits = torch.fill(torch.empty(batch_size, seq_len, vocab_size), torch.rand(1).item()) outputs = MagicMock() outputs.logits = logits labels = torch.tensor([[1, 0, 0], [0, 1, -100]]) ce_loss = torch.nn.functional.cross_entropy( logits.view(-1, vocab_size), labels.view(-1), ignore_index=-100, reduction="mean" ) # We need to account for the logits shift operation so we don't consider the first tokens # in each row of the batch num_items_in_batch = 3 # Dft loss predicted_dft_loss = dft_loss(outputs, labels, num_items_in_batch) # If we have just two tokens in our vocab and all logits are the same, # dft scales the ce_loss per token by 0.5. So the dft_loss should be ce_loss/2 torch.testing.assert_close(ce_loss / 2.0, predicted_dft_loss, atol=1e-4, rtol=1e-4) class TestDataCollatorForLanguageModeling(TrlTestCase): def test_basic_padding(self): """Test basic padding functionality without completion masks.""" collator = DataCollatorForLanguageModeling(pad_token_id=0) examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3], [4, 5, -100]])) def test_completion_mask(self): """Test completion mask functionality.""" collator = DataCollatorForLanguageModeling(pad_token_id=0) examples = [ {"input_ids": [1, 2, 3], "completion_mask": [0, 1, 1]}, {"input_ids": [4, 5], "completion_mask": [0, 1]}, ] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, 2, 3], [-100, 5, -100]])) def test_completion_only_loss_disabled(self): """Test behavior when completion_only_loss is disabled.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, completion_only_loss=False) examples = [ {"input_ids": [1, 2, 3], "completion_mask": [0, 1, 1]}, {"input_ids": [4, 5], "completion_mask": [0, 1]}, ] result = collator(examples) # Labels should not be masked when completion_only_loss=False assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3], [4, 5, -100]])) def test_padding_free_mode(self): """Test padding-free mode where sequences are concatenated.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, padding_free=True) examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "position_ids", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 4, 5]])) torch.testing.assert_close(result["position_ids"], torch.tensor([[0, 1, 2, 0, 1]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, 2, 3, -100, 5]])) def test_padding_free_with_completion_mask(self): """Test padding-free mode with completion masks.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, padding_free=True) examples = [ {"input_ids": [1, 2, 3], "completion_mask": [0, 0, 1]}, {"input_ids": [4, 5], "completion_mask": [1, 1]}, ] result = collator(examples) assert set(result.keys()) == {"input_ids", "position_ids", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 4, 5]])) torch.testing.assert_close(result["position_ids"], torch.tensor([[0, 1, 2, 0, 1]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, -100, 3, -100, 5]])) def test_packing(self): """Test that when using packing with position_ids, attention_mask is dropped with fa2.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, padding_free=True) # Simulate packed sequences with position_ids that restart (typical of BFD packing) examples = [ {"input_ids": [1, 2, 3, 4, 5, 6], "seq_lengths": [3, 3]}, {"input_ids": [7, 8, 9, 10, 11], "seq_lengths": [4, 1]}, ] result = collator(examples) assert set(result.keys()) == {"input_ids", "position_ids", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]])) torch.testing.assert_close(result["position_ids"], torch.tensor([[0, 1, 2, 0, 1, 2, 0, 1, 2, 3, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, 2, 3, -100, 5, 6, -100, 8, 9, 10, -100]])) def test_pad_to_multiple_of(self): """Test padding to multiple of specified value.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, pad_to_multiple_of=4) examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 0], [4, 5, 0, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1, 0], [1, 1, 0, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3, -100], [4, 5, -100, -100]])) def test_pad_to_multiple_of_and_padding_free(self): """Test padding to multiple of specified value.""" collator = DataCollatorForLanguageModeling(pad_token_id=0, padding_free=True, pad_to_multiple_of=4) examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "position_ids", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 4, 5, 0, 0, 0]])) torch.testing.assert_close(result["position_ids"], torch.tensor([[0, 1, 2, 0, 1, 0, 0, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, 2, 3, -100, 5, -100, -100, -100]])) def test_custom_position_ids_but_no_padding_free(self): """Test that custom position_ids are ignored if padding_free is False.""" collator = DataCollatorForLanguageModeling(pad_token_id=0) examples = [{"input_ids": [1, 2, 3], "seq_lengths": [1, 2]}, {"input_ids": [4, 5], "seq_lengths": [2]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3], [4, 5, -100]])) def test_single_example(self): """Test collator with a single example.""" collator = DataCollatorForLanguageModeling(pad_token_id=0) examples = [{"input_ids": [1, 2, 3, 4]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3, 4]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1, 1]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3, 4]])) def test_different_pad_token_id(self): """Test with different pad token ID.""" collator = DataCollatorForLanguageModeling(pad_token_id=999) examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] result = collator(examples) assert set(result.keys()) == {"input_ids", "attention_mask", "labels"} torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 999]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[1, 2, 3], [4, 5, -100]])) def test_assistant_masks(self): """Test handling of assistant masks in examples.""" collator = DataCollatorForLanguageModeling(pad_token_id=0) examples = [ {"input_ids": [1, 2, 3], "assistant_masks": [0, 1, 1]}, {"input_ids": [4, 5], "assistant_masks": [0, 1]}, ] result = collator(examples) torch.testing.assert_close(result["input_ids"], torch.tensor([[1, 2, 3], [4, 5, 0]])) torch.testing.assert_close(result["attention_mask"], torch.tensor([[1, 1, 1], [1, 1, 0]])) torch.testing.assert_close(result["labels"], torch.tensor([[-100, 2, 3], [-100, 5, -100]])) def test_single_example_single_doc(self): batch_seq_lengths = [[5]] result = DataCollatorForLanguageModeling.get_position_ids_from_packed_seq_lengths(batch_seq_lengths) assert len(result) == 1 assert torch.equal(result[0], torch.arange(5)) def test_single_example_multiple_docs(self): batch_seq_lengths = [[3, 2]] result = DataCollatorForLanguageModeling.get_position_ids_from_packed_seq_lengths(batch_seq_lengths) assert len(result) == 1 # First sequence: 0, 1, 2; second sequence: 0, 1 assert torch.equal(result[0], torch.tensor([0, 1, 2, 0, 1])) def test_multiple_examples(self): batch_seq_lengths = [[2, 2], [3]] result = DataCollatorForLanguageModeling.get_position_ids_from_packed_seq_lengths(batch_seq_lengths) assert len(result) == 2 assert torch.equal(result[0], torch.tensor([0, 1, 0, 1])) assert torch.equal(result[1], torch.arange(3)) class TestSFTTrainer(TrlTestCase): def test_init_with_training_arguments(self): dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") args = TrainingArguments(output_dir=self.tmp_dir, report_to="none") SFTTrainer(model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=args, train_dataset=dataset) @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Cohere2ForCausalLM", pytest.param( "trl-internal-testing/tiny-Glm4MoeForCausalLM", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.0.0"), reason="GLM4 tokenizer requires transformers>=5.0.0", ), ), "trl-internal-testing/tiny-GptOssForCausalLM", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "trl-internal-testing/tiny-Qwen3MoeForCausalLM", ], ) def test_train(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer(model=model_id, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # Special case for harmony def test_train_gpt_oss(self): # Get the dataset dataset = load_dataset("trl-internal-testing/harmony", "language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-GptOssForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_model(self): # Instantiate the model model = AutoModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32", ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_dft_loss(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, loss_type="dft", learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", eval_strategy="steps", eval_steps=3, ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_moe_model_with_aux_loss(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, report_to="none", model_init_kwargs={"output_router_logits": True}, ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen3MoeForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss and aux loss are not None assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["aux_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_formatting_func(self): # Dummy formatting function def formatting_prompts_func(example): chosen, rejected = example["chosen"], example["rejected"] return f"### Chosen: {chosen}\n### Rejected: {rejected}" # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_implicit_prompt_preference", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, formatting_func=formatting_prompts_func, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_model_dtype(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, model_init_kwargs={"dtype": torch.float16}, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): # For some reasonn model.layers.0.input_layernorm.weight doesn't change in GitHub Actions but does # locally. We ignore this parameter for now if "layernorm" in n: continue new_param = trainer.model.get_parameter(n) # Check the torch dtype assert new_param.dtype == torch.float16 assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_dense_with_peft_config_lora(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize( "peft_type", [ "prompt_tuning", "prefix_tuning", "prompt_encoder", ], ) @require_peft def test_train_with_peft_config_prompt_tuning(self, peft_type): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer, p-tuning doesn't support gradient checkpointing training_args = SFTConfig(bf16=False, output_dir=self.tmp_dir, report_to="none", gradient_checkpointing=False) if peft_type == "prompt_tuning": peft_config = PromptTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=4, tokenizer_name_or_path="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", ) elif peft_type == "prefix_tuning": if parse_version(peft.__version__) <= Version("0.17.1"): pytest.xfail( "Prefix tuning with device_map='auto' is broken in peft 0.17.1 and below. See " "https://github.com/huggingface/peft/issues/2821" ) peft_config = PrefixTuningConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=4, ) elif peft_type == "prompt_encoder": peft_config = PromptEncoderConfig( task_type=TaskType.CAUSAL_LM, num_virtual_tokens=4, encoder_hidden_size=model.config.hidden_size, # This will be overwritten below ) trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=peft_config, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" else: # We expect the peft parameters to be different assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_moe_with_peft_config(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-GptOssForCausalLM" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(target_parameters=["mlp.experts.down_proj", "mlp.experts.gate_up_proj"]), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_peft def test_train_peft_model(self): # Get the base model model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") # Get the base model parameter names base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Turn the model into a peft model lora_config = LoraConfig() model = get_peft_model(model, lora_config) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer(model=model, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # In practice, this test is the same as `test_train_dense_with_peft_config_lora`, since gradient checkpointing is # enabled by default in `SFTTrainer`. We keep it as a regression guard: if the default ever changes, we still # explicitly test PEFT + gradient checkpointing, which has caused issues in the past. @require_peft def test_train_with_peft_config_and_gradient_checkpointing(self): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, gradient_checkpointing=True, report_to="none") trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize("use_reentrant", [True, False]) @require_peft def test_train_with_peft_config_and_gradient_checkpointing_reentrant(self, use_reentrant): # Get the base model parameter names model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" model = AutoModelForCausalLM.from_pretrained(model_id, dtype="float32") base_param_names = [f"base_model.model.{n}" for n, _ in model.named_parameters()] # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": use_reentrant}, report_to="none", ) trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, peft_config=LoraConfig(), ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n in base_param_names: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "base_layer" not in n: # We expect the peft parameters to be different (except for the base layer) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_liger_kernel def test_train_with_liger(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, use_liger_kernel=True, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_torch_accelerator @require_liger_kernel def test_compute_loss_skip_logits_on_eval_without_metrics_with_liger(self): dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train[:1]") training_args = SFTConfig( output_dir=self.tmp_dir, use_liger_kernel=False, report_to="none", max_length=8, bf16=False, ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, compute_metrics=None, ) trainer.args.use_liger_kernel = True trainer.model.eval() captured = {} def mock_super_compute_loss(model, inputs, return_outputs=False, num_items_in_batch=None): captured["skip_logits"] = inputs.get("skip_logits") dummy_loss = torch.tensor(1.0, requires_grad=True) dummy_outputs = MagicMock() dummy_outputs.token_accuracy = None dummy_outputs.logits = torch.randn(1, 5, trainer.model.config.vocab_size) return (dummy_loss, dummy_outputs) inputs = { "input_ids": torch.tensor([[1, 2, 3, 4, 5]]), "labels": torch.tensor([[1, 2, 3, 4, 5]]), "attention_mask": torch.tensor([[1, 1, 1, 1, 1]]), } with patch("transformers.Trainer.compute_loss", side_effect=mock_super_compute_loss): trainer.compute_loss(trainer.model, inputs) assert captured["skip_logits"] is True @require_torch_accelerator @require_liger_kernel def test_predict_does_not_skip_logits_with_liger(self): dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train[:1]") training_args = SFTConfig( output_dir=self.tmp_dir, use_liger_kernel=False, report_to="none", max_length=8, bf16=False, ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, compute_metrics=None, ) trainer.args.use_liger_kernel = True trainer.model.eval() captured = {} def mock_super_compute_loss(model, inputs, return_outputs=False, num_items_in_batch=None): captured["skip_logits"] = inputs.get("skip_logits") dummy_loss = torch.tensor(1.0, requires_grad=True) dummy_outputs = (dummy_loss, torch.randn(1, 5, trainer.model.config.vocab_size)) return (dummy_loss, dummy_outputs) with patch("transformers.Trainer.compute_loss", side_effect=mock_super_compute_loss): trainer.predict(trainer.train_dataset) assert captured["skip_logits"] is False def test_train_with_non_chatml_conversational_data(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") # Rename role/content to from/value to ensure SFT works with non-chatML conversational data def rename_fields(example: list[dict]): return {"conversations": [{"from": m["role"], "value": m["content"]} for m in example["messages"]]} dataset = dataset.map(rename_fields, remove_columns="messages") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_pretokenized_data(self): # Get the dataset model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" tokenizer = AutoTokenizer.from_pretrained(model_id) dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") def tokenize_example(example): return tokenizer(example["text"]) # Apply tokenization tokenized_dataset = dataset.map(tokenize_example, remove_columns=["text"]) # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer(model=model_id, args=training_args, train_dataset=tokenized_dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_iterable_dataset(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train", streaming=True) # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, max_steps=3, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @require_kernels @require_ampere_or_newer # Flash attention 2 requires Ampere or newer GPUs def test_train_padding_free(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, padding_free=True, model_init_kwargs={"attn_implementation": "kernels-community/flash-attn2"}, bf16=True, # flash_attention_2 only supports bf16 and fp16 report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize("packing_strategy", ["bfd", "wrapped"]) @ignore_warnings(message="You are using packing, but the attention implementation is not.*", category=UserWarning) @ignore_warnings(message="Padding-free training is enabled, but the attention.*", category=UserWarning) def test_train_packing(self, packing_strategy): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, packing=True, packing_strategy=packing_strategy, max_length=10, report_to="none" ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @ignore_warnings(message="You are using packing, but the attention implementation is not.*", category=UserWarning) @ignore_warnings(message="Padding-free training is enabled, but the attention.*", category=UserWarning) def test_eval_packing(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, packing=True, max_length=64, report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Check the number of sequences in train and eval datasets num_train_seqs = sum(len(x) for x in trainer.train_dataset["seq_lengths"]) num_eval_seqs = sum(len(x) for x in trainer.eval_dataset["seq_lengths"]) assert num_train_seqs == 17 # we should still have 17 seqs assert num_eval_seqs == 2 # we should still have 2 seqs # Check that all sequences are shorter than the max length assert all(sum(x) <= 64 for x in trainer.train_dataset["seq_lengths"]) assert all(sum(x) <= 64 for x in trainer.eval_dataset["seq_lengths"]) # Check the number of sequences in train and eval datasets assert len(trainer.train_dataset["input_ids"]) == 3 # w/ this dataset, we end up with 46 seqs assert len(trainer.eval_dataset["input_ids"]) == 1 # w/ this dataset, we end up with 6 seqs @ignore_warnings(message="You are using packing, but the attention implementation is not.*", category=UserWarning) @ignore_warnings(message="Padding-free training is enabled, but the attention.*", category=UserWarning) def test_only_train_packing(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, packing=True, eval_packing=False, max_length=64, report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Check the number of sequences in train dataset num_train_seqs = sum(len(x) for x in trainer.train_dataset["seq_lengths"]) assert num_train_seqs == 17 # we should still have 17 seqs # We expect eval dataset not having "seq_lengths" as eval_packing is False assert "seq_lengths" not in trainer.eval_dataset # Check that all sequences are shorter than the max length assert all(sum(x) <= 64 for x in trainer.train_dataset["seq_lengths"]) # Check the number of sequences in train and eval datasets assert len(trainer.train_dataset["input_ids"]) == 3 # w/ this dataset, we end up with 46 seqs assert len(trainer.eval_dataset["input_ids"]) == 2 # w/ this dataset, we end up with 6 seqs def test_train_with_chat_template_kwargs(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") tokenizer = AutoTokenizer.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5") # The following template is a simplified version of the Qwen chat template, where an additional argument # `role_capital` is used to control the capitalization of roles. tokenizer.chat_template = '{%- if messages[0]["role"] == "system" -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\n" + messages[0]["content"] + "<|im_end|>\\n" }}{%- else -%} {{ "<|im_start|>" + ("SYSTEM" if role_capital else "system") + "\\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\\n" }}{%- endif -%}{%- for message in messages -%} {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) -%} {{ "<|im_start|>" + (message.role.upper() if role_capital else message.role) + "\\n" + message.content + "<|im_end|>\\n" }} {%- elif message.role == "assistant" -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") }} {%- if message.content -%} {{ "\\n" + message.content }} {%- endif -%} {{ "<|im_end|>\\n" }} {%- elif message.role == "tool" -%} {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") -%} {{ "<|im_start|>" + ("USER" if role_capital else "user") }} {%- endif -%} {{ "\\n\\n" + message.content + "\\n" }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") -%} {{ "<|im_end|>\\n" }} {%- endif -%} {%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%} {{ "<|im_start|>" + ("ASSISTANT" if role_capital else "assistant") + "\\n" }}{%- endif -%}' dataset = dataset.add_column( "chat_template_kwargs", [{"role_capital": bool(i % 2)} for i in range(len(dataset))] ) assert "chat_template_kwargs" in dataset.features trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, processing_class=tokenizer, ) # Assert trainer uses the same chat template as tokenizer assert trainer.processing_class.chat_template == tokenizer.chat_template # Assert chat_template is applied for i in range(2): role = "SYSTEM" if i else "system" system_prompt = ( f"<|im_start|>{role}\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>" ) system_prompt_ids = trainer.processing_class(system_prompt)["input_ids"] assert trainer.train_dataset[i]["input_ids"][: len(system_prompt_ids)] == system_prompt_ids # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_assistant_only(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, assistant_only_loss=True, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_completion_only(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, completion_only_loss=True, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_completion_only_harmony(self): # Get the dataset dataset = load_dataset("trl-internal-testing/harmony", "prompt_completion", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, completion_only_loss=True, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-GptOssForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_assistant_only_and_completion_only(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_prompt_completion", split="train") # To test this case, we need to add user messages in the completion (they'll be masked in the loss) def add_to_completion(example): example["completion"].append(example["prompt"][0]) example["completion"].append(example["completion"][0]) return example dataset = dataset.map(add_to_completion) # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, assistant_only_loss=True, completion_only_loss=True, report_to="none" ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_assistant_only_iterable_dataset(self): # Get the dataset dataset = load_dataset( "trl-internal-testing/zen", "conversational_language_modeling", split="train", streaming=True ) # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, assistant_only_loss=True, max_steps=3, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen3ForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_set_chat_template_from_model(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, chat_template_path="Qwen/Qwen3-4B", report_to="none") # trl-internal-testing/tiny-GPTNeoXForCausalLM doesn't have a chat template set by default trainer = SFTTrainer( model="trl-internal-testing/tiny-GPTNeoXForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_set_chat_template_from_path(self, lazy_shared_datadir): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, chat_template_path=str(lazy_shared_datadir / "template.jinja"), report_to="none", ) # trl-internal-testing/tiny-GPTNeoXForCausalLM doesn't have a chat template set by default trainer = SFTTrainer( model="trl-internal-testing/tiny-GPTNeoXForCausalLM", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" # Check that the template saved in the output directory is the same as the one used for training template_path = pathlib.Path(self.tmp_dir) / "checkpoint-9" / "chat_template.jinja" assert template_path.exists(), f"Chat template not found at {template_path}" with open(template_path) as f: template_content = f.read() with open(training_args.chat_template_path) as f: original_template_content = f.read() assert template_content == original_template_content, "Chat template content does not match the original" def test_train_toolcall_data(self): # Get the dataset dataset = load_dataset("trl-internal-testing/toolcall", "language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_toolcall_data_as_json(self): # Tabular backends (Arrow/Parquet) can insert `None` for missing keys in nested structures. # If `tools` is stored as a list of dicts and examples use different dict schemas, nulls may # be introduced and break tool processing. This test ensures we also support `tools` provided # as a list of dicts. # Get the dataset dataset = load_dataset("trl-internal-testing/toolcall", "language_modeling", split="train") def convert_to_json(example): return {"tools": json.loads(example["tools"])} dataset = dataset.map(convert_to_json) # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_train_with_eval(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], ) # Train the model trainer.train() # Check that the eval loss is not None assert trainer.state.log_history[0]["eval_loss"] is not None def test_train_with_multiple_eval_dataset(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset={"data1": dataset["test"], "data2": dataset["test"]}, ) # Train the model trainer.train() # Check that the eval losses are not None assert trainer.state.log_history[-3]["eval_data1_loss"] is not None assert trainer.state.log_history[-2]["eval_data2_loss"] is not None def test_train_with_compute_metrics(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling") def dummy_compute_metrics(eval_pred): return {"my_metric": 0.123} # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, eval_strategy="steps", eval_steps=3, report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], compute_metrics=dummy_compute_metrics, ) # Train the model trainer.train() # Check that the custom metric is logged assert trainer.state.log_history[-2]["eval_my_metric"] == 0.123 # In practice, this test is the same as `test_train`, since gradient checkpointing is enabled by default in # `SFTTrainer`. We keep it as a regression guard: if the default ever changes, we still explicitly test gradient # checkpointing, which has caused issues in the past. def test_train_with_gradient_checkpointing(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, gradient_checkpointing=True, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" @pytest.mark.parametrize("use_reentrant", [True, False]) def test_train_with_gradient_checkpointing_reentrant(self, use_reentrant): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, gradient_checkpointing=True, gradient_checkpointing_kwargs={"use_reentrant": use_reentrant}, report_to="none", ) trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" def test_tag_added(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, ) for tag in ["sft", "trl"]: assert tag in trainer.model.model_tags @require_peft def test_tag_added_peft(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", train_dataset=dataset, peft_config=LoraConfig(), ) for tag in ["sft", "trl"]: assert tag in trainer.model.model_tags @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", # "trl-internal-testing/tiny-Idefics2ForConditionalGeneration", high memory peak, skipped for now # "trl-internal-testing/tiny-Idefics3ForConditionalGeneration", high memory peak, skipped for now "trl-internal-testing/tiny-LlavaForConditionalGeneration", "trl-internal-testing/tiny-LlavaNextForConditionalGeneration", "trl-internal-testing/tiny-Qwen2VLForConditionalGeneration", "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", # "trl-internal-testing/tiny-SmolVLMForConditionalGeneration", seems not to support bf16 properly pytest.param( "trl-internal-testing/tiny-Qwen3VLForConditionalGeneration", marks=[ pytest.mark.skipif( Version(transformers.__version__) < Version("4.57.0"), reason="Qwen3-VL series were introduced in transformers-4.57.0", ), pytest.mark.xfail( Version("5.0.0") <= Version(transformers.__version__) < Version("5.1.0"), reason="Upstream transformers bug (transformers#43334) in 5.0.x; fixed in 5.1.0", ), ], ), pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.2.0"), reason="Qwen3.5 models were introduced in transformers-5.2.0", ), ), ], ) @require_vision def test_train_vlm(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-image", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, max_length=None, # for VLMs, truncating can remove image tokens, leading to errors report_to="none", ) trainer = SFTTrainer(model=model_id, args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # For some reason, these params are not updated. This is probably not related to TRL, but to # the model itself. We should investigate this further, but for now we just skip these params. # fmt: off if ( model_id == "trl-internal-testing/tiny-Gemma3ForConditionalGeneration" and "model.vision_tower.vision_model.head" in n or model_id == "trl-internal-testing/tiny-LlavaForConditionalGeneration" and "model.vision_tower.vision_model.post_layernorm" in n or model_id == "trl-internal-testing/tiny-LlavaForConditionalGeneration" and "vision_tower.vision_model.encoder.layers.1" in n or model_id == "trl-internal-testing/tiny-LlavaNextForConditionalGeneration" and "model.vision_tower.vision_model.post_layernorm" in n or model_id == "trl-internal-testing/tiny-LlavaNextForConditionalGeneration" and "vision_tower.vision_model.encoder.layers.1" in n or model_id == "trl-internal-testing/tiny-Qwen3VLForConditionalGeneration" and "model.visual.deepstack_merger_list" in n ): # fmt: on continue assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @pytest.mark.xfail( parse_version(transformers.__version__) < parse_version("4.57.0"), reason="Mixing text-only and image+text examples is only supported in transformers >= 4.57.0", strict=False, ) @require_vision def test_train_vlm_multi_image(self, model_id): # Get the dataset dataset = load_dataset( "trl-internal-testing/zen-multi-image", "conversational_prompt_completion", split="train" ) # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_length=None, # for VLMs, truncating can remove image tokens, leading to errors report_to="none", ) trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", # Special case for Gemma, as it uses token_type_ids, and we need to ensure they are properly in the collator: "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", ], ) @require_vision def test_train_vlm_prompt_completion(self, model_id): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-image", "conversational_prompt_completion", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_length=None, # for VLMs, truncating can remove image tokens, leading to errors report_to="none", ) trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" # Gemma 3n uses a timm encoder, making it difficult to create a smaller variant for testing. # To ensure coverage, we run tests on the full model but mark them as slow to exclude from default runs. @pytest.mark.slow @require_vision @pytest.mark.skip(reason="Model google/gemma-3n-E2B-it is gated and requires HF token") def test_train_vlm_gemma_3n(self): # Get the dataset dataset = load_dataset("trl-internal-testing/zen-image", "conversational_language_modeling", split="train") # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, learning_rate=0.1, # use higher lr because gradients are tiny and default lr can stall updates max_length=None, # for VLMs, truncating can remove image tokens, leading to errors per_device_train_batch_size=1, # VLM training is memory intensive, reduce batch size to avoid OOM model_init_kwargs={"dtype": "bfloat16"}, report_to="none", ) trainer = SFTTrainer(model="google/gemma-3n-E2B-it", args=training_args, train_dataset=dataset) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if "model.audio_tower" in n or "model.embed_audio" in n: # The audio embedding parameters are not updated because this dataset contains no audio data continue assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", ], ) @pytest.mark.parametrize( "dataset_config", ["conversational_language_modeling", "conversational_prompt_completion", "standard_prompt_completion"], ) @require_vision def test_train_vlm_text_only_data(self, model_id, dataset_config): # Get the dataset dataset = load_dataset("trl-internal-testing/zen", dataset_config, split="train") # Initialize the trainer training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model=model_id, args=training_args, train_dataset=dataset, ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if n.startswith("model.visual"): torch.testing.assert_close(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is updated" else: assert not torch.allclose(param, new_param, rtol=1e-12, atol=1e-12), f"Param {n} is not updated" @require_peft def test_prompt_tuning(self): """Test that SFT works with Prompt Tuning.""" dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer( model="trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", args=training_args, train_dataset=dataset, peft_config=PromptEncoderConfig(task_type=TaskType.CAUSAL_LM, num_virtual_tokens=8), ) # Save initial parameters to check they change during training previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training completed successfully assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["mean_token_accuracy"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if "base_model" in n: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "prompt_encoder" in n: # We expect the peft parameters to be different assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" else: raise ValueError(f"Unexpected parameter {n} in model: {trainer.model}") @require_peft @require_bitsandbytes def test_peft_with_quantization(self): # Get the base model model_id = "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5" quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16, ) model = AutoModelForCausalLM.from_pretrained( model_id, dtype="float32", quantization_config=quantization_config, ) # Get the dataset dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") # Initialize the trainer with the already configured PeftModel training_args = SFTConfig(output_dir=self.tmp_dir, learning_rate=0.1, report_to="none") trainer = SFTTrainer(model=model, args=training_args, train_dataset=dataset, peft_config=LoraConfig()) # Save initial parameters to check they change during training previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training completed successfully assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["mean_token_accuracy"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) # In bitsandbytes, bias parameters are automatically cast to the input dtype during the forward pass if # their dtype doesn’t match. This causes the module to change unexpectedly during the first forward pass of # the training. To handle this, we cast these specific bias parameters to float32 before comparison. # https://github.com/bitsandbytes-foundation/bitsandbytes/blob/45553f7392e524eacf400b132cfe01261f6477be/bitsandbytes/nn/modules.py#L518 # We still need to investigate why the compute dtype ends up being different than for these parameters. if n in [ "base_model.model.model.layers.1.self_attn.k_proj.bias", "base_model.model.model.layers.1.self_attn.q_proj.base_layer.bias", "base_model.model.model.layers.1.self_attn.v_proj.base_layer.bias", ]: param = param.float() if "lora" not in n: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "lora" in n: # We expect the peft parameters to be different assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" else: raise ValueError(f"Unexpected parameter {n} in model: {trainer.model}") @require_peft def test_prompt_tuning_peft_model(self): """Test that SFT works with Prompt Tuning and a pre-converted PeftModel""" model = AutoModelForCausalLM.from_pretrained("trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", dtype="float32") model = get_peft_model(model, PromptEncoderConfig(task_type=TaskType.CAUSAL_LM, num_virtual_tokens=8)) dataset = load_dataset("trl-internal-testing/zen", "standard_language_modeling", split="train") training_args = SFTConfig(output_dir=self.tmp_dir, report_to="none") trainer = SFTTrainer(model=model, args=training_args, train_dataset=dataset) # Save initial parameters to check they change during training previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} trainer.train() # Check that training completed successfully assert trainer.state.log_history[-1]["train_loss"] is not None assert trainer.state.log_history[-1]["mean_token_accuracy"] is not None # Check the peft params have changed and the base model params have not changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) if "base_model" in n: # We expect the base model parameters to be the same torch.testing.assert_close(param, new_param), f"Parameter {n} has changed" elif "prompt_encoder" in n: # We expect the peft parameters to be different assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" else: raise ValueError(f"Unexpected parameter {n} in model: {trainer.model}") @pytest.mark.slow @require_torch_accelerator @require_peft class TestSFTTrainerSlow(TrlTestCase): def setup_method(self): self.train_dataset = load_dataset("stanfordnlp/imdb", split="train[:10%]") self.eval_dataset = load_dataset("stanfordnlp/imdb", split="test[:10%]") self.max_length = 128 self.peft_config = LoraConfig( lora_alpha=16, lora_dropout=0.1, r=8, bias="none", task_type="CAUSAL_LM", ) def teardown_method(self): gc.collect() backend_empty_cache(torch_device) gc.collect() @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) def test_sft_trainer_transformers_mp(self, model_name, packing): """ Simply tests if passing a transformers model to `SFTTrainer` loads and runs the trainer as expected in mixed precision. """ training_args = SFTConfig( output_dir=self.tmp_dir, logging_strategy="no", report_to="none", per_device_train_batch_size=2, max_steps=10, fp16=True, # this is sufficient to enable amp packing=packing, max_length=self.max_length, ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32") tokenizer = AutoTokenizer.from_pretrained(model_name) trainer = SFTTrainer( model, args=training_args, processing_class=tokenizer, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, ) trainer.train() release_memory(model, trainer) @pytest.mark.parametrize("device_map", [{"": 0}, "auto"]) @pytest.mark.parametrize( "gradient_checkpointing_kwargs", [None, {"use_reentrant": False}, {"use_reentrant": True}] ) @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_torch_multi_accelerator def test_sft_trainer_transformers_mp_gc_device_map( self, model_name, packing, gradient_checkpointing_kwargs, device_map ): """ Simply tests if passing a transformers model to `SFTTrainer` loads and runs the trainer as expected in mixed precision + different scenarios of gradient_checkpointing (single, multi-gpu, etc). """ training_args = SFTConfig( output_dir=self.tmp_dir, logging_strategy="no", report_to="none", per_device_train_batch_size=2, max_steps=10, packing=packing, max_length=self.max_length, fp16=True, # this is sufficient to enable amp gradient_checkpointing=True, # default, here for clarity gradient_checkpointing_kwargs=gradient_checkpointing_kwargs, ) model = AutoModelForCausalLM.from_pretrained(model_name, dtype="float32", device_map=device_map) tokenizer = AutoTokenizer.from_pretrained(model_name) trainer = SFTTrainer( model, args=training_args, processing_class=tokenizer, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, ) trainer.train() release_memory(model, trainer) @pytest.mark.parametrize( "gradient_checkpointing_kwargs", [None, {"use_reentrant": False}, {"use_reentrant": True}] ) @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_peft @require_bitsandbytes def test_sft_trainer_transformers_mp_gc_peft_qlora(self, model_name, packing, gradient_checkpointing_kwargs): """ Simply tests if passing a transformers model + PEFT + bnb to `SFTTrainer` loads and runs the trainer as expected in mixed precision + different scenarios of gradient_checkpointing. """ training_args = SFTConfig( output_dir=self.tmp_dir, logging_strategy="no", report_to="none", per_device_train_batch_size=2, max_steps=10, packing=packing, max_length=self.max_length, gradient_checkpointing=True, # default, here for clarity gradient_checkpointing_kwargs=gradient_checkpointing_kwargs, ) quantization_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16) model = AutoModelForCausalLM.from_pretrained( model_name, dtype="float32", quantization_config=quantization_config ) tokenizer = AutoTokenizer.from_pretrained(model_name) trainer = SFTTrainer( model, args=training_args, processing_class=tokenizer, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, peft_config=self.peft_config, ) assert isinstance(trainer.model, PeftModel) trainer.train() release_memory(model, trainer) @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_peft @require_bitsandbytes def test_sft_trainer_with_chat_format_qlora(self, model_name, packing): """ Simply tests if using setup_chat_format with a transformers model + peft + bnb config to `SFTTrainer` loads and runs the trainer as expected. """ train_dataset = load_dataset("trl-internal-testing/dolly-chatml-sft", split="train") training_args = SFTConfig( packing=packing, max_length=self.max_length, output_dir=self.tmp_dir, logging_strategy="no", report_to="none", per_device_train_batch_size=2, max_steps=10, ) quantization_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16) model = AutoModelForCausalLM.from_pretrained( model_name, dtype="float32", quantization_config=quantization_config ) tokenizer = AutoTokenizer.from_pretrained(model_name) trainer = SFTTrainer( model, args=training_args, processing_class=tokenizer, train_dataset=train_dataset, peft_config=self.peft_config, ) assert isinstance(trainer.model, PeftModel) trainer.train() release_memory(model, trainer) @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_liger_kernel def test_sft_trainer_with_liger(self, model_name, packing): """ Tests if passing use_liger=True to SFTConfig loads and runs the trainer with AutoLigerKernelForCausalLM as expected. """ import importlib def cleanup_liger_patches(trainer): """Clean up liger_kernel patches by reloading the model's specific module""" try: # Get the specific module that was used by the trainer's model module_path = trainer.model.__module__ reload_module = importlib.import_module(module_path) importlib.reload(reload_module) except Exception: pass # Continue if reload fails training_args = SFTConfig( output_dir=self.tmp_dir, logging_strategy="no", report_to="none", per_device_train_batch_size=2, max_steps=2, packing=packing, max_length=self.max_length, use_liger_kernel=True, ) trainer = SFTTrainer( model_name, args=training_args, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset, ) # Ensure cleanup of liger patches after the test try: trainer.train() release_memory(trainer.model, trainer) finally: cleanup_liger_patches(trainer) @pytest.mark.parametrize("packing", [True, False]) @pytest.mark.parametrize( "model_name", [ "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-MistralForCausalLM-0.2", ], ) @require_torch_accelerator def test_train_offloading(self, model_name, packing): """Test that activation offloading works with SFTTrainer.""" # Initialize the trainer training_args = SFTConfig( output_dir=self.tmp_dir, activation_offloading=True, report_to="none", per_device_train_batch_size=2, max_steps=2, packing=packing, max_length=self.max_length, ) trainer = SFTTrainer( model=model_name, args=training_args, train_dataset=self.train_dataset, eval_dataset=self.eval_dataset ) # Save the initial parameters to compare them later previous_trainable_params = {n: param.clone() for n, param in trainer.model.named_parameters()} # Train the model trainer.train() # Check that the training loss is not None assert trainer.state.log_history[-1]["train_loss"] is not None # Check the params have changed for n, param in previous_trainable_params.items(): new_param = trainer.model.get_parameter(n) assert not torch.allclose(param, new_param), f"Parameter {n} has not changed" release_memory(trainer.model, trainer) ================================================ FILE: tests/test_skills.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path import pytest from trl.skills import install_skill, list_agent_names, list_skills, resolve_target_path, uninstall_skill from trl.skills.skills import _get_trl_skills_dir class TestGetTrlSkillsDir: """Tests for _get_trl_skills_dir function.""" def test_returns_path_object(self): """Test that returns a Path object.""" skills_dir = _get_trl_skills_dir() assert isinstance(skills_dir, Path) def test_directory_exists(self): """Test that the returned directory exists.""" skills_dir = _get_trl_skills_dir() assert skills_dir.exists(), f"Skills directory does not exist: {skills_dir}" def test_is_directory(self): """Test that the returned path is a directory.""" skills_dir = _get_trl_skills_dir() assert skills_dir.is_dir(), f"Skills path is not a directory: {skills_dir}" def test_contains_skills_module(self): """Test that the path ends with 'skills' (the module name).""" skills_dir = _get_trl_skills_dir() assert skills_dir.name == "skills" class TestListSkills: """Tests for list_skills function.""" def test_returns_list(self): """Test that list_skills returns a list.""" skills = list_skills() assert isinstance(skills, list) def test_contains_trl_training(self): """Test that list_skills includes the trl-training skill.""" skills = list_skills() assert "trl-training" in skills def test_skills_are_sorted(self): """Test that skills are returned in sorted order.""" skills = list_skills() assert skills == sorted(skills) def test_with_custom_directory(self, tmp_path): """Test list_skills with a custom directory.""" # Create fake skills (tmp_path / "skill1").mkdir() (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") (tmp_path / "skill2").mkdir() (tmp_path / "skill2" / "SKILL.md").write_text("# Skill 2") (tmp_path / "not-a-skill").mkdir() # No SKILL.md skills = list_skills(tmp_path) assert skills == ["skill1", "skill2"] def test_empty_directory(self, tmp_path): """Test list_skills with an empty directory.""" skills = list_skills(tmp_path) assert skills == [] def test_nonexistent_directory(self, tmp_path): """Test list_skills with a non-existent directory.""" nonexistent = tmp_path / "nonexistent" skills = list_skills(nonexistent) assert skills == [] def test_ignores_files(self, tmp_path): """Test that list_skills ignores files, only returns directories.""" (tmp_path / "skill1").mkdir() (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") (tmp_path / "not-a-skill.txt").write_text("Not a skill") skills = list_skills(tmp_path) assert skills == ["skill1"] def test_requires_skill_md(self, tmp_path): """Test that directories without SKILL.md are ignored.""" (tmp_path / "has-skill-md").mkdir() (tmp_path / "has-skill-md" / "SKILL.md").write_text("# Valid") (tmp_path / "no-skill-md").mkdir() (tmp_path / "no-skill-md" / "readme.md").write_text("# Invalid") skills = list_skills(tmp_path) assert skills == ["has-skill-md"] class TestInstallSkill: """Tests for install_skill function.""" def test_basic_installation(self, tmp_path): """Test basic skill installation.""" target_dir = tmp_path / "target" result = install_skill("trl-training", target_dir) assert result is True assert (target_dir / "trl-training").exists() assert (target_dir / "trl-training" / "SKILL.md").exists() def test_creates_target_directory(self, tmp_path): """Test that install_skill creates the target directory if it doesn't exist.""" target_dir = tmp_path / "nested" / "target" install_skill("trl-training", target_dir) assert target_dir.exists() assert (target_dir / "trl-training").exists() def test_skill_not_found(self, tmp_path): """Test that install_skill raises FileNotFoundError for non-existent skill.""" target_dir = tmp_path / "target" with pytest.raises(FileNotFoundError, match="Skill 'nonexistent' not found"): install_skill("nonexistent", target_dir) def test_skill_already_exists_without_force(self, tmp_path): """Test that install_skill raises FileExistsError if skill exists and force=False.""" target_dir = tmp_path / "target" # Install once install_skill("trl-training", target_dir) # Try to install again without force with pytest.raises(FileExistsError, match="already installed"): install_skill("trl-training", target_dir, force=False) def test_force_overwrites_existing(self, tmp_path): """Test that install_skill with force=True overwrites existing skill.""" target_dir = tmp_path / "target" # Install once install_skill("trl-training", target_dir) # Modify the installed skill marker_file = target_dir / "trl-training" / "marker.txt" marker_file.write_text("This should be removed") # Install again with force result = install_skill("trl-training", target_dir, force=True) assert result is True assert (target_dir / "trl-training").exists() assert not marker_file.exists() # Marker should be gone def test_force_overwrites_symlink(self, tmp_path): """Test that install_skill with force=True can overwrite a symlink.""" target_dir = tmp_path / "target" target_dir.mkdir() # Create a symlink symlink = target_dir / "trl-training" symlink.symlink_to(_get_trl_skills_dir() / "trl-training") # Install with force should replace symlink with copy result = install_skill("trl-training", target_dir, force=True) assert result is True assert (target_dir / "trl-training").exists() assert not (target_dir / "trl-training").is_symlink() def test_skill_not_directory(self, tmp_path): """Test that install_skill raises ValueError if skill is not a directory.""" source_dir = tmp_path / "source" source_dir.mkdir() target_dir = tmp_path / "target" # Create a file instead of directory (source_dir / "fake-skill").write_text("not a directory") with pytest.raises(ValueError, match="is not a directory"): install_skill("fake-skill", target_dir, source=source_dir) def test_preserves_directory_structure(self, tmp_path): """Test that install_skill preserves the skill's directory structure.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create a skill with subdirectories skill_dir = source_dir / "test-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Test") (skill_dir / "subdir").mkdir() (skill_dir / "subdir" / "file.txt").write_text("content") install_skill("test-skill", target_dir, source=source_dir) assert (target_dir / "test-skill" / "SKILL.md").exists() assert (target_dir / "test-skill" / "subdir" / "file.txt").exists() assert (target_dir / "test-skill" / "subdir" / "file.txt").read_text() == "content" def test_install_to_same_directory_fails(self, tmp_path): """Test that installing to the same directory as source is handled correctly.""" source_dir = tmp_path / "skills" source_dir.mkdir() # Create a skill skill_dir = source_dir / "test-skill" skill_dir.mkdir() (skill_dir / "SKILL.md").write_text("# Test") # Try to install to same directory (should fail with exists error) with pytest.raises(FileExistsError): install_skill("test-skill", source_dir, source=source_dir, force=False) class TestUninstallSkill: """Tests for uninstall_skill function.""" def test_basic_uninstallation(self, tmp_path): """Test basic skill uninstallation.""" target_dir = tmp_path / "target" # Install first install_skill("trl-training", target_dir) assert (target_dir / "trl-training").exists() # Uninstall result = uninstall_skill("trl-training", target_dir) assert result is True assert not (target_dir / "trl-training").exists() def test_skill_not_installed(self, tmp_path): """Test that uninstall_skill raises FileNotFoundError for non-existent skill.""" target_dir = tmp_path / "target" target_dir.mkdir() with pytest.raises(FileNotFoundError, match="not installed"): uninstall_skill("nonexistent", target_dir) def test_uninstall_from_nonexistent_directory(self, tmp_path): """Test uninstall_skill when target directory doesn't exist.""" target_dir = tmp_path / "nonexistent" with pytest.raises(FileNotFoundError, match="not installed"): uninstall_skill("trl-training", target_dir) def test_uninstall_removes_all_contents(self, tmp_path): """Test that uninstall removes the entire skill directory.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create a skill with multiple files skill_dir = source_dir / "test-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Test") (skill_dir / "file1.txt").write_text("content1") (skill_dir / "subdir").mkdir() (skill_dir / "subdir" / "file2.txt").write_text("content2") # Install and uninstall install_skill("test-skill", target_dir, source=source_dir) uninstall_skill("test-skill", target_dir) assert not (target_dir / "test-skill").exists() # Target directory itself should still exist assert target_dir.exists() def test_uninstall_doesnt_affect_other_skills(self, tmp_path): """Test that uninstalling one skill doesn't affect others.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create two skills for skill_name in ["skill1", "skill2"]: skill_dir = source_dir / skill_name skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text(f"# {skill_name}") # Install both install_skill("skill1", target_dir, source=source_dir) install_skill("skill2", target_dir, source=source_dir) # Uninstall one uninstall_skill("skill1", target_dir) # Check that only skill1 is removed assert not (target_dir / "skill1").exists() assert (target_dir / "skill2").exists() class TestIntegration: """Integration tests for skills functions.""" def test_full_workflow(self, tmp_path): """Test complete install -> list -> uninstall workflow.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create skills for i in range(3): skill_dir = source_dir / f"skill{i}" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text(f"# Skill {i}") # List available skills available = list_skills(target=source_dir) assert available == ["skill0", "skill1", "skill2"] # Install skills for skill in available: install_skill(skill, target_dir, source=source_dir) # List installed skills installed_dirs = [d.name for d in target_dir.iterdir() if d.is_dir()] assert sorted(installed_dirs) == ["skill0", "skill1", "skill2"] # Uninstall one skill uninstall_skill("skill1", target_dir) # Verify installed_dirs = [d.name for d in target_dir.iterdir() if d.is_dir()] assert sorted(installed_dirs) == ["skill0", "skill2"] def test_install_uninstall_cycle(self, tmp_path): """Test that we can install and uninstall the same skill multiple times.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create skill skill_dir = source_dir / "test-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Test") # Install -> Uninstall -> Install -> Uninstall for _ in range(2): install_skill("test-skill", target_dir, source=source_dir) assert (target_dir / "test-skill").exists() uninstall_skill("test-skill", target_dir) assert not (target_dir / "test-skill").exists() def test_force_reinstall_workflow(self, tmp_path): """Test the workflow of using force to update an installed skill.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create initial skill version skill_dir = source_dir / "test-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Version 1") # Install install_skill("test-skill", target_dir, source=source_dir) assert (target_dir / "test-skill" / "SKILL.md").read_text() == "# Version 1" # Update source skill (skill_dir / "SKILL.md").write_text("# Version 2") # Force reinstall install_skill("test-skill", target_dir, source=source_dir, force=True) assert (target_dir / "test-skill" / "SKILL.md").read_text() == "# Version 2" class TestEdgeCases: """Tests for edge cases and special scenarios.""" def test_skill_with_special_characters_in_name(self, tmp_path): """Test handling skills with special characters in names.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create skill with hyphens and underscores (common in skill names) skill_name = "test-skill_v2" skill_dir = source_dir / skill_name skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Test") # Should work fine install_skill(skill_name, target_dir, source=source_dir) assert (target_dir / skill_name).exists() uninstall_skill(skill_name, target_dir) assert not (target_dir / skill_name).exists() def test_empty_skill_directory(self, tmp_path): """Test installing a skill with only SKILL.md (no other files).""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" skill_dir = source_dir / "minimal-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Minimal") install_skill("minimal-skill", target_dir, source=source_dir) assert (target_dir / "minimal-skill" / "SKILL.md").exists() # Should only contain SKILL.md files = list((target_dir / "minimal-skill").iterdir()) assert len(files) == 1 assert files[0].name == "SKILL.md" def test_skill_with_hidden_files(self, tmp_path): """Test that hidden files are preserved during installation.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" skill_dir = source_dir / "test-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Test") (skill_dir / ".hidden").write_text("hidden content") install_skill("test-skill", target_dir, source=source_dir) assert (target_dir / "test-skill" / ".hidden").exists() assert (target_dir / "test-skill" / ".hidden").read_text() == "hidden content" def test_list_skills_with_symlinks(self, tmp_path): """Test that list_skills handles symlinked skill directories.""" source_dir = tmp_path / "source" skills_dir = tmp_path / "skills" skills_dir.mkdir() # Create a real skill skill_dir = source_dir / "real-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Real") # Create symlink to it (skills_dir / "linked-skill").symlink_to(skill_dir) # list_skills should include symlinked skills if they have SKILL.md skills = list_skills(target=skills_dir) assert "linked-skill" in skills class TestListAgentNames: """Tests for list_agent_names function.""" def test_returns_list(self): """Test that list_agent_names returns a list.""" agents = list_agent_names() assert isinstance(agents, list) def test_contains_expected_agents(self): """Test that list includes expected agent names.""" agents = list_agent_names() assert "claude" in agents assert "codex" in agents assert "opencode" in agents def test_agents_are_sorted(self): """Test that agent names are sorted.""" agents = list_agent_names() assert agents == sorted(agents) class TestResolveTargetPath: """Tests for resolve_target_path function.""" def test_resolve_agent_name_project_scope(self): """Test resolving agent name with project scope.""" path = resolve_target_path("claude", "project") assert path == Path("./.claude/skills").expanduser().resolve() def test_resolve_agent_name_global_scope(self): """Test resolving agent name with global scope.""" path = resolve_target_path("claude", "global") assert path == Path("~/.claude/skills").expanduser().resolve() def test_resolve_custom_path_string(self): """Test resolving custom path as string.""" path = resolve_target_path("/custom/path", "project") assert path == Path("/custom/path").resolve() def test_resolve_custom_path_object(self): """Test resolving Path object.""" custom = Path("/custom/path") path = resolve_target_path(custom, "project") assert path == Path("/custom/path").resolve() def test_resolve_path_with_tilde(self): """Test that tilde expansion works.""" path = resolve_target_path("~/my/skills", "project") assert path == Path("~/my/skills").expanduser().resolve() assert "~" not in str(path) def test_all_predefined_agents(self): """Test that all predefined agents can be resolved.""" for agent in list_agent_names(): for scope in ["project", "global"]: path = resolve_target_path(agent, scope) assert isinstance(path, Path) assert path.is_absolute() def test_invalid_scope_for_predefined_agent(self): """Test invalid scope raises ValueError for predefined agents.""" with pytest.raises(ValueError, match="Invalid scope"): resolve_target_path("claude", "invalid") class TestHighLevelAPI: """Tests for the new high-level API (target/scope instead of Path).""" def test_list_skills_with_target_string(self, tmp_path): """Test list_skills with target as string (custom path).""" # Create skills in target (tmp_path / "skill1").mkdir() (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") skills = list_skills(target=str(tmp_path), scope="project") assert skills == ["skill1"] def test_list_skills_with_target_path(self, tmp_path): """Test list_skills with target as Path object.""" (tmp_path / "skill1").mkdir() (tmp_path / "skill1" / "SKILL.md").write_text("# Skill 1") skills = list_skills(target=tmp_path, scope="project") assert skills == ["skill1"] def test_list_skills_without_target(self): """Test list_skills without target lists TRL's built-in skills.""" skills = list_skills() assert isinstance(skills, list) assert "trl-training" in skills def test_install_skill_with_target_string(self, tmp_path): """Test install_skill with target as string.""" result = install_skill("trl-training", target=str(tmp_path), scope="project") assert result is True assert (tmp_path / "trl-training").exists() def test_install_skill_with_target_path(self, tmp_path): """Test install_skill with target as Path object.""" result = install_skill("trl-training", target=tmp_path, scope="project") assert result is True assert (tmp_path / "trl-training").exists() def test_install_skill_with_force(self, tmp_path): """Test install_skill with force parameter.""" install_skill("trl-training", target=tmp_path) # Install again with force result = install_skill("trl-training", target=tmp_path, force=True) assert result is True def test_uninstall_skill_with_target_string(self, tmp_path): """Test uninstall_skill with target as string.""" install_skill("trl-training", target=tmp_path) result = uninstall_skill("trl-training", target=str(tmp_path), scope="project") assert result is True assert not (tmp_path / "trl-training").exists() def test_uninstall_skill_with_target_path(self, tmp_path): """Test uninstall_skill with target as Path object.""" install_skill("trl-training", target=tmp_path) result = uninstall_skill("trl-training", target=tmp_path, scope="project") assert result is True assert not (tmp_path / "trl-training").exists() def test_install_with_custom_source(self, tmp_path): """Test install_skill with custom source parameter.""" source_dir = tmp_path / "source" target_dir = tmp_path / "target" # Create custom skill skill_dir = source_dir / "custom-skill" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text("# Custom") result = install_skill("custom-skill", target=target_dir, source=source_dir) assert result is True assert (target_dir / "custom-skill").exists() ================================================ FILE: tests/test_skills_cli.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import pytest from trl.skills import install_skill from trl.skills.cli import add_skills_subcommands, cmd_install, cmd_list, cmd_uninstall class TestCLICommands: """Tests for CLI command handlers.""" def test_cmd_list_without_target(self, capsys): """Test cmd_list without target (lists TRL skills).""" args = argparse.Namespace(target=None, scope="project") result = cmd_list(args) captured = capsys.readouterr() assert result == 0 assert "TRL (available for installation)" in captured.out assert "trl-training" in captured.out assert "Use 'trl skills install" in captured.out def test_cmd_list_with_target(self, tmp_path, capsys): """Test cmd_list with target (lists installed skills).""" # Install a skill install_skill("trl-training", target=tmp_path) args = argparse.Namespace(target=str(tmp_path), scope="project") result = cmd_list(args) captured = capsys.readouterr() assert result == 0 assert "trl-training" in captured.out assert str(tmp_path) in captured.out def test_cmd_list_empty_target(self, tmp_path, capsys): """Test cmd_list with empty target directory.""" args = argparse.Namespace(target=str(tmp_path), scope="project") result = cmd_list(args) captured = capsys.readouterr() assert result == 0 assert "No skills installed" in captured.out def test_cmd_install_single_skill(self, tmp_path, capsys): """Test cmd_install with single skill.""" args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 0 assert "✓" in captured.out assert "1/1 skills installed" in captured.out assert (tmp_path / "trl-training").exists() def test_cmd_install_all_skills(self, tmp_path, capsys): """Test cmd_install with --all flag.""" args = argparse.Namespace(skill=None, all=True, target=str(tmp_path), scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 0 assert "✓" in captured.out assert "installed successfully" in captured.out assert (tmp_path / "trl-training").exists() def test_cmd_install_no_skill_or_all(self, capsys): """Test cmd_install without skill name or --all flag.""" args = argparse.Namespace(skill=None, all=False, target="/tmp/test", scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 1 assert "Error: Either provide a skill name or use --all" in captured.out def test_cmd_install_both_skill_and_all(self, capsys): """Test cmd_install with both skill name and --all (error).""" args = argparse.Namespace(skill="trl-training", all=True, target="/tmp/test", scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 1 assert "Cannot specify both" in captured.out def test_cmd_install_nonexistent_skill(self, tmp_path, capsys): """Test cmd_install with non-existent skill.""" args = argparse.Namespace(skill="nonexistent", all=False, target=str(tmp_path), scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 1 assert "✗" in captured.out assert "0/1 skills installed" in captured.out def test_cmd_install_already_exists(self, tmp_path, capsys): """Test cmd_install when skill already exists without force.""" # Install once install_skill("trl-training", target=tmp_path) args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=False) result = cmd_install(args) captured = capsys.readouterr() assert result == 1 assert "✗" in captured.out assert "Use --force to overwrite" in captured.out def test_cmd_install_with_force(self, tmp_path, capsys): """Test cmd_install with --force to overwrite.""" # Install once install_skill("trl-training", target=tmp_path) args = argparse.Namespace(skill="trl-training", all=False, target=str(tmp_path), scope="project", force=True) result = cmd_install(args) captured = capsys.readouterr() assert result == 0 assert "✓" in captured.out assert "1/1 skills installed" in captured.out def test_cmd_uninstall_success(self, tmp_path, capsys): """Test cmd_uninstall with installed skill.""" # Install first install_skill("trl-training", target=tmp_path) args = argparse.Namespace(skill="trl-training", target=str(tmp_path), scope="project") result = cmd_uninstall(args) captured = capsys.readouterr() assert result == 0 assert "✓" in captured.out assert "has been removed" in captured.out assert not (tmp_path / "trl-training").exists() def test_cmd_uninstall_not_installed(self, tmp_path, capsys): """Test cmd_uninstall when skill is not installed.""" args = argparse.Namespace(skill="nonexistent", target=str(tmp_path), scope="project") result = cmd_uninstall(args) captured = capsys.readouterr() assert result == 1 assert "✗" in captured.out assert "Error:" in captured.out def test_cmd_install_creates_target_directory(self, tmp_path, capsys): """Test cmd_install creates target directory if it doesn't exist.""" # Custom path that doesn't exist yet target_path = tmp_path / "new_directory" assert not target_path.exists() args = argparse.Namespace( skill="trl-training", all=False, target=str(target_path), scope="project", force=False ) result = cmd_install(args) captured = capsys.readouterr() assert result == 0 assert "✓" in captured.out assert target_path.exists() def test_cmd_uninstall_invalid_target(self, capsys): """Test cmd_uninstall with non-existent path.""" args = argparse.Namespace(skill="trl-training", target="/nonexistent/invalid/path", scope="project") result = cmd_uninstall(args) captured = capsys.readouterr() assert result == 1 assert "✗" in captured.out class TestCLIArgumentParsing: """Tests for CLI argument parsing setup.""" def test_add_skills_subcommands_creates_parsers(self): """Test that add_skills_subcommands creates the expected subparsers.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) # Test that we can parse expected commands args = parser.parse_args(["list"]) assert args.command == "list" assert hasattr(args, "func") args = parser.parse_args(["install", "trl-training", "--target", "claude"]) assert args.command == "install" assert args.skill == "trl-training" assert args.target == "claude" args = parser.parse_args(["uninstall", "trl-training", "--target", "claude"]) assert args.command == "uninstall" assert args.skill == "trl-training" def test_list_command_optional_target(self): """Test that list command has optional target.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) # Should work without target args = parser.parse_args(["list"]) assert args.target is None # Should work with target args = parser.parse_args(["list", "--target", "claude"]) assert args.target == "claude" def test_install_command_requires_target(self): """Test that install command requires target.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) # Should fail without target with pytest.raises(SystemExit): parser.parse_args(["install", "trl-training"]) def test_scope_choices(self): """Test that scope parameter accepts valid choices.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) # Valid scopes args = parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "project"]) assert args.scope == "project" args = parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "global"]) assert args.scope == "global" # Invalid scope should fail with pytest.raises(SystemExit): parser.parse_args(["install", "trl-training", "--target", "claude", "--scope", "invalid"]) def test_install_all_flag(self): """Test install --all flag.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) args = parser.parse_args(["install", "--all", "--target", "claude"]) assert args.all is True assert args.skill is None def test_install_force_flag(self): """Test install --force flag.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) args = parser.parse_args(["install", "trl-training", "--target", "claude", "--force"]) assert args.force is True def test_default_scope_is_project(self): """Test that default scope is 'project'.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest="command") add_skills_subcommands(subparsers) args = parser.parse_args(["install", "trl-training", "--target", "claude"]) assert args.scope == "project" ================================================ FILE: tests/test_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap from io import StringIO from unittest.mock import patch import pytest import torch import transformers from packaging.version import Version from transformers import AutoModelForCausalLM, AutoModelForImageTextToText from transformers.utils import is_peft_available from trl import ModelConfig from trl.trainer.utils import ( RepeatSampler, entropy_from_logits, flush_left, flush_right, forward_masked_logits, generate_model_card, get_peft_config, hash_module, nanstd, pad, print_prompt_completions_sample, selective_log_softmax, shuffle_sequence_dict, split_pixel_values_by_grid, split_tensor_dict, unsplit_pixel_values_by_grid, use_adapter, ) from .testing_utils import TrlTestCase, require_peft, require_rich if is_peft_available(): from peft import AutoPeftModelForCausalLM, LoraConfig @require_peft class TestUseAdapter(TrlTestCase): def test_disables_on_none(self): model = AutoPeftModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-PeftModel", adapter_name="my_adapter" ) input_ids = torch.tensor([[1, 2, 3], [4, 5, 6]]) with model.disable_adapter(): expected = model(input_ids).logits with use_adapter(model, None): output = model(input_ids).logits assert torch.equal(output, expected) def test_restores_previous_adapter(self): model = AutoPeftModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-PeftModel", adapter_name="my_adapter" ) input_ids = torch.tensor([[1, 2, 3], [4, 5, 6]]) expected = model(input_ids).logits with use_adapter(model, "my_adapter"): pass output = model(input_ids).logits assert torch.equal(output, expected) with use_adapter(model, None): pass output = model(input_ids).logits assert torch.equal(output, expected) def test_with_multiple_adapters(self): model = AutoPeftModelForCausalLM.from_pretrained( "trl-internal-testing/tiny-PeftModel", adapter_name="my_adapter_1" ) model.load_adapter("trl-internal-testing/tiny-PeftModel-2", "my_adapter_2") input_ids = torch.tensor([[1, 2, 3], [4, 5, 6]]) model.set_adapter("my_adapter_1") # should be a no-op, but let's keep it for clarity expected_1 = model(input_ids).logits model.set_adapter("my_adapter_2") expected_2 = model(input_ids).logits with use_adapter(model, "my_adapter_1"): output_1 = model(input_ids).logits with use_adapter(model, "my_adapter_2"): output_2 = model(input_ids).logits assert torch.equal(output_1, expected_1) assert torch.equal(output_2, expected_2) class TestPad(TrlTestCase): def test_pad_1_dim_left(self): x = torch.tensor([1, 2, 3]) y = torch.tensor([4, 5]) output = pad((x, y), padding_value=0, padding_side="left") expected = torch.tensor([[1, 2, 3], [0, 4, 5]]) assert torch.equal(output, expected) def test_pad_1_dim_right(self): x = torch.tensor([1, 2, 3]) y = torch.tensor([4, 5]) output = pad((x, y), padding_value=0, padding_side="right") expected = torch.tensor([[1, 2, 3], [4, 5, 0]]) assert torch.equal(output, expected) def test_pad_2_dim_left(self): x = torch.tensor([[1, 2], [3, 4]]) y = torch.tensor([[5, 6]]) output = pad((x, y), padding_value=0, padding_side="left") expected = torch.tensor( [ [[1, 2], [3, 4]], [[0, 0], [5, 6]], ] ) assert torch.equal(output, expected) def test_pad_2_dim_right(self): x = torch.tensor([[1, 2], [3, 4]]) y = torch.tensor([[5, 6]]) output = pad((x, y), padding_value=0, padding_side="right") expected = torch.tensor( [ [[1, 2], [3, 4]], [[5, 6], [0, 0]], ] ) assert torch.equal(output, expected) def test_pad_2_dim_right_multidim(self): x = torch.tensor([[1, 2], [3, 4]]) y = torch.tensor([[5]]) output = pad((x, y), padding_value=0, padding_side="right") expected = torch.tensor( [ [[1, 2], [3, 4]], [[5, 0], [0, 0]], ] ) assert torch.equal(output, expected) def test_pad_to_multiple_of_1(self): x = torch.tensor([1, 2, 3]) y = torch.tensor([4, 5]) # Max length is 3, pad to multiple of 4 output = pad((x, y), padding_value=0, padding_side="right", pad_to_multiple_of=4) expected = torch.tensor([[1, 2, 3, 0], [4, 5, 0, 0]]) assert torch.equal(output, expected) def test_pad_to_multiple_of_2(self): x = torch.tensor([1, 2, 3, 4, 5]) y = torch.tensor([6, 7, 8]) # Max length is 3, pad to multiple of 4 output = pad((x, y), padding_value=0, padding_side="right", pad_to_multiple_of=4) expected = torch.tensor([[1, 2, 3, 4, 5, 0, 0, 0], [6, 7, 8, 0, 0, 0, 0, 0]]) assert torch.equal(output, expected) def test_pad_to_multiple_of_side_left(self): x = torch.tensor([1, 2, 3, 4, 5]) y = torch.tensor([6, 7, 8]) # Max length is 3, pad to multiple of 4 output = pad((x, y), padding_value=0, padding_side="left", pad_to_multiple_of=4) expected = torch.tensor([[0, 0, 0, 1, 2, 3, 4, 5], [0, 0, 0, 0, 0, 6, 7, 8]]) assert torch.equal(output, expected) def test_pad_to_multiple_of_no_extra_padding(self): x = torch.tensor([1, 2, 3, 4]) y = torch.tensor([5, 6, 7, 8]) # Already multiple of 4 output = pad((x, y), padding_value=0, padding_side="left", pad_to_multiple_of=4) expected = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]]) assert torch.equal(output, expected) class TestHashModule(TrlTestCase): def test_hash_module_deterministic_across_order(self): class ModAB(torch.nn.Module): def __init__(self, a: torch.Tensor, b: torch.Tensor): super().__init__() self.a = torch.nn.Parameter(a) self.b = torch.nn.Parameter(b) class ModBA(torch.nn.Module): def __init__(self, a: torch.Tensor, b: torch.Tensor): super().__init__() self.b = torch.nn.Parameter(b) self.a = torch.nn.Parameter(a) a = torch.tensor([[1.0, 2.0]]) b = torch.tensor([3.0]) assert hash_module(ModAB(a, b)) == hash_module(ModBA(a, b)) def test_hash_module_changes_with_value(self): class Mod(torch.nn.Module): def __init__(self, value: float): super().__init__() self.weight = torch.nn.Parameter(torch.tensor([value, 2.0])) assert hash_module(Mod(1.0)) != hash_module(Mod(1.5)) def test_hash_module_includes_dtype(self): class Mod(torch.nn.Module): def __init__(self, dtype: torch.dtype): super().__init__() self.weight = torch.nn.Parameter(torch.tensor([1.0, 2.0], dtype=dtype)) assert hash_module(Mod(torch.float32)) != hash_module(Mod(torch.float16)) def test_hash_module_tiny_model_twice(self): model_id = "trl-internal-testing/tiny-GptOssForCausalLM" model_a = AutoModelForCausalLM.from_pretrained(model_id) model_b = AutoModelForCausalLM.from_pretrained(model_id) assert hash_module(model_a) == hash_module(model_b) def test_hash_module_tiny_model_change_layer(self): model_id = "trl-internal-testing/tiny-GptOssForCausalLM" model = AutoModelForCausalLM.from_pretrained(model_id) h1 = hash_module(model) with torch.no_grad(): model.lm_head.weight.add_(0.01) h2 = hash_module(model) assert h1 != h2 @require_peft class TestGetPEFTConfig(TrlTestCase): def test_create_peft_config_use_peft_false(self): """Test that when use_peft is False, the function returns None.""" model_args = ModelConfig(use_peft=False) peft_config = get_peft_config(model_args) assert peft_config is None def test_create_peft_config_use_peft_true(self): """Test that when use_peft is True, the function returns a LoraConfig object.""" # Provide non-default values to the model config for testing peft_kwargs = { "lora_r": 8, "lora_alpha": 16, "lora_dropout": 0.1, "lora_task_type": "SEQ_CLS", "use_rslora": True, "lora_target_modules": ["up_proj", "down_proj"], "lora_modules_to_save": ["up_proj"], } model_args = ModelConfig(use_peft=True, **peft_kwargs) peft_config = get_peft_config(model_args) assert isinstance(peft_config, LoraConfig) for arg, value in peft_kwargs.items(): # Test that lists of modules are converted to sets if arg == "lora_target_modules": value = set(value) # Rename the argument to match the LoraConfig attribute name if arg in ["lora_r", "lora_task_type", "lora_target_modules", "lora_modules_to_save"]: arg = arg[len("lora_") :] if arg.startswith("lora_") else arg assert getattr(peft_config, arg) == value class TestNanStd(TrlTestCase): def test_nanstd_ignores_nans(self): x = torch.tensor([1.0, 2.0, 3.0, float("nan")]) result = nanstd(x) torch.testing.assert_close(result, torch.tensor(1.0)) def test_nanstd_dim_and_keepdim(self): x = torch.tensor([[1.0, float("nan")], [3.0, 5.0]]) result = nanstd(x, dim=1, keepdim=True) assert torch.isnan(result[0, 0]) torch.testing.assert_close(result[1, 0], torch.tensor(1.4142135), rtol=1e-5, atol=1e-6) def test_nanstd_all_nan(self): x = torch.tensor([float("nan"), float("nan")]) result = nanstd(x) assert torch.isnan(result) class TestGenerateModelCard(TrlTestCase): def test_full(self): model_card = generate_model_card( base_model="username/my_base_model", model_name="my_model", hub_model_id="username/my_hub_model", dataset_name="username/my_dataset", tags=["trl", "trainer-tag"], wandb_url="https://wandb.ai/username/project_id/runs/abcd1234", trackio_url="https://huggingface.co/spaces/username/space_id", comet_url="https://www.comet.com/username/project_id/experiment_id", trainer_name="My Trainer", trainer_citation="@article{my_trainer, ...}", paper_title="My Paper", paper_id="1234.56789", ) card_text = str(model_card) assert "[username/my_base_model](https://huggingface.co/username/my_base_model)" in card_text assert "my_model" in card_text assert 'pipeline("text-generation", model="username/my_hub_model", device="cuda")' in card_text assert "datasets: username/my_dataset" in card_text assert "](https://wandb.ai/username/project_id/runs/abcd1234)" in card_text assert "](https://huggingface.co/spaces/username/space_id)" in card_text assert "](https://www.comet.com/username/project_id/experiment_id" in card_text assert "My Trainer" in card_text assert "```bibtex\n@article{my_trainer, ...}\n```" in card_text assert "[My Paper](https://huggingface.co/papers/1234.56789)" in card_text def test_val_none(self): model_card = generate_model_card( base_model=None, model_name="my_model", hub_model_id="username/my_hub_model", dataset_name=None, tags=[], wandb_url=None, trackio_url=None, comet_url=None, trainer_name="My Trainer", trainer_citation=None, paper_title=None, paper_id=None, ) card_text = str(model_card) assert "my_model" in card_text assert 'pipeline("text-generation", model="username/my_hub_model", device="cuda")' in card_text assert "My Trainer" in card_text class TestFlushLeft(TrlTestCase): def test_basic_case(self): mask = torch.tensor([[0, 0, 1, 1, 1], [0, 1, 1, 0, 0]]) tensor1 = torch.tensor([[0, 0, 2, 3, 4], [0, 5, 6, 0, 0]]) tensor2 = torch.tensor([[0, 0, 7, 8, 9], [0, 10, 11, 0, 0]]) new_mask, new_tensor1, new_tensor2 = flush_left(mask, tensor1, tensor2) expected_mask = torch.tensor([[1, 1, 1], [1, 1, 0]]) expected_tensor1 = torch.tensor([[2, 3, 4], [5, 6, 0]]) expected_tensor2 = torch.tensor([[7, 8, 9], [10, 11, 0]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) assert torch.equal(new_tensor2, expected_tensor2) def test_single_row(self): mask = torch.tensor([[0, 0, 1, 1]]) tensor1 = torch.tensor([[0, 0, 2, 3]]) new_mask, new_tensor1 = flush_left(mask, tensor1) expected_mask = torch.tensor([[1, 1]]) expected_tensor1 = torch.tensor([[2, 3]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) def test_no_shift_needed(self): mask = torch.tensor([[1, 1, 0, 0], [1, 0, 0, 0]]) tensor1 = torch.tensor([[5, 6, 0, 0], [7, 0, 0, 0]]) new_mask, new_tensor1 = flush_left(mask, tensor1) expected_mask = torch.tensor([[1, 1], [1, 0]]) expected_tensor1 = torch.tensor([[5, 6], [7, 0]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) def test_no_tensors(self): mask = torch.tensor([[0, 0, 1, 1, 1], [0, 1, 1, 0, 0]]) new_mask = flush_left(mask) expected_mask = torch.tensor([[1, 1, 1], [1, 1, 0]]) assert torch.equal(new_mask, expected_mask) class TestFlushRight(TrlTestCase): def test_basic_case(self): mask = torch.tensor([[1, 1, 1, 0, 0], [0, 0, 1, 1, 0]]) tensor1 = torch.tensor([[2, 3, 4, 0, 0], [0, 0, 5, 6, 0]]) tensor2 = torch.tensor([[7, 8, 9, 0, 0], [0, 0, 10, 11, 0]]) new_mask, new_tensor1, new_tensor2 = flush_right(mask, tensor1, tensor2) expected_mask = torch.tensor([[1, 1, 1], [0, 1, 1]]) expected_tensor1 = torch.tensor([[2, 3, 4], [0, 5, 6]]) expected_tensor2 = torch.tensor([[7, 8, 9], [0, 10, 11]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) assert torch.equal(new_tensor2, expected_tensor2) def test_single_row(self): mask = torch.tensor([[1, 1, 0, 0]]) tensor1 = torch.tensor([[2, 3, 0, 0]]) new_mask, new_tensor1 = flush_right(mask, tensor1) expected_mask = torch.tensor([[1, 1]]) expected_tensor1 = torch.tensor([[2, 3]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) def test_no_shift_needed(self): mask = torch.tensor([[0, 0, 1, 1], [0, 0, 0, 1]]) tensor1 = torch.tensor([[0, 0, 5, 6], [0, 0, 0, 7]]) new_mask, new_tensor1 = flush_right(mask, tensor1) expected_mask = torch.tensor([[1, 1], [0, 1]]) expected_tensor1 = torch.tensor([[5, 6], [0, 7]]) assert torch.equal(new_mask, expected_mask) assert torch.equal(new_tensor1, expected_tensor1) def test_no_tensors(self): mask = torch.tensor([[1, 1, 1, 0, 0], [0, 0, 1, 1, 0]]) new_mask = flush_right(mask) expected_mask = torch.tensor([[1, 1, 1], [0, 1, 1]]) assert torch.equal(new_mask, expected_mask) class TestRepeatRandomSampler(TrlTestCase): def test_sampler(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=2) # Should output something like [4, 4, 3, 3, 0, 0, 1, 1, 2, 2, 6, 6, 5, 5] sampled = list(sampler) # Check that the length is doubled assert len(sampled) == 2 * len(dataset) # Check that all indexes are present assert set(sampled) == set(range(len(dataset))) # Check that each element is repeated twice assert all(sampled[i] == sampled[i + 1] for i in range(0, len(sampled), 2)) def test_sampler_no_shuffle(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=2, shuffle=False) sampled = list(sampler) expected = [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6] assert sampled == expected def test_sampler_no_repeat(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=1) # Should output something like [4, 3, 0, 1, 2, 6, 5] sampled = list(sampler) # Check that the length is the same assert len(sampled) == len(dataset) # Check that all indexes are present assert set(sampled) == set(range(len(dataset))) def test_sampler_with_batch_size(self): dataset = ["a", "b", "c", "d", "e", "f", "g", "h"] sampler = RepeatSampler(dataset, mini_repeat_count=1, batch_size=2, repeat_count=2) # Should output something like [4, 3, 4, 3, 0, 1, 0, 1, 2, 6, 2, 6, 5, 7, 5, 7] sampled = list(sampler) # Check that the length is doubled assert len(sampled) == 2 * len(dataset) # Check that all indexes are present assert set(sampled) == set(range(len(dataset))) # Check that each element is repeated as expected assert all(sampled[i : i + 1] == sampled[i + 2 : i + 3] for i in range(0, len(sampled), 4)) def test_sampler_with_batch_size_and_drop(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=1, batch_size=2, repeat_count=2) # Should output something like [4, 3, 4, 3, 0, 1, 0, 1, 2, 6, 2, 6] sampled = list(sampler) # Check that the length is doubled assert len(sampled) == 2 * ( len(dataset) - 1 ) # one element is dropped, because it's not enough to form a batch assert len(sampler) == len(sampled) # the length should be the same as the sampled length # Check that the sampled indexes are a subset of the dataset indexes assert set(sampled).issubset(set(range(len(dataset)))) # Check that each element is repeated as expected assert all(sampled[i : i + 1] == sampled[i + 2 : i + 3] for i in range(0, len(sampled), 4)) def test_sampler_with_mini_repeat_count_and_batch_size_1(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=2, batch_size=3, repeat_count=2) # Should output something like [4, 4, 3, 3, 0, 0, 4, 4, 3, 3, 0, 0, # 1, 1, 2, 2, 6, 6, 1, 1, 2, 2, 6, 6] sampled = list(sampler) # Check that the length is quadrupled assert len(sampled) == 4 * (len(dataset) - 1) # 1 element is dropped, because it's not enough to form a batch assert len(sampler) == len(sampled) # the length should be the same as the sampled length # Check that the sampled indexes are a subset of the dataset indexes assert set(sampled).issubset(set(range(len(dataset)))) # Check that each element is repeated as expected assert all(sampled[i] == sampled[i + 1] for i in range(0, len(sampled), 2)) # Check that the batch is repeated as expected assert sampled[0:6] == sampled[6:12] assert sampled[12:18] == sampled[18:24] def test_sampler_with_mini_repeat_count_and_batch_size_2(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=3, batch_size=2, repeat_count=2) # Should output something like [4, 4, 4, 3, 3, 3, 4, 4, 4, 3, 3, 3, # 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, # 2, 2, 2, 6, 6, 6, 2, 2, 2, 6, 6, 6] sampled = list(sampler) # Check that the length is sextupled assert len(sampled) == 6 * (len(dataset) - 1) # 1 element is dropped, because it's not enough to form a batch assert len(sampler) == len(sampled) # the length should be the same as the sampled length # Check that the sampled indexes are a subset of the dataset indexes assert set(sampled).issubset(set(range(len(dataset)))) # Check that each element is repeated as expected assert all(sampled[i] == sampled[i + 1] == sampled[i + 2] for i in range(0, len(sampled), 3)) # Check that the batch is repeated as expected assert sampled[0:6] == sampled[6:12] assert sampled[12:18] == sampled[18:24] assert sampled[24:30] == sampled[30:36] def test_sampler_with_mini_repeat_count_and_batch_size_3(self): dataset = ["a", "b", "c", "d", "e", "f", "g"] sampler = RepeatSampler(dataset, mini_repeat_count=2, batch_size=2, repeat_count=3) # Should output something like [4, 4, 3, 3, 4, 4, 3, 3, 4, 4, 3, 3, # 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, # 2, 2, 6, 6, 2, 2, 6, 6, 2, 2, 6, 6] sampled = list(sampler) # Check that the length is sextupled assert len(sampled) == 6 * (len(dataset) - 1) # 1 element is dropped, because it's not enough to form a batch # Check that the sampled indexes are a subset of the dataset indexes assert set(sampled).issubset(set(range(len(dataset)))) # Check that each element is repeated as expected assert all(sampled[i] == sampled[i + 1] for i in range(0, len(sampled), 2)) # Check that the batch is repeated as expected assert sampled[0:4] == sampled[4:8] == sampled[8:12] assert sampled[12:16] == sampled[16:20] == sampled[20:24] assert sampled[24:28] == sampled[28:32] == sampled[32:36] class TestEntropyFromLogits(TrlTestCase): @pytest.mark.parametrize("shape", [(768,), (32, 768), (8, 16, 768), (2, 4, 8, 768)]) @pytest.mark.parametrize("chunk_size", [1, 16]) @pytest.mark.parametrize("dtype", [torch.float64, torch.float32, torch.float16, torch.bfloat16]) def test_entropy_from_logits_2_dims(self, dtype, chunk_size, shape): logits = torch.randn(*shape, dtype=dtype) if dtype in (torch.float64, torch.float32): p = logits.softmax(-1) entropy = -torch.sum(p * p.log(), dim=-1) else: logps = logits.log_softmax(dim=-1) entropy = -(torch.exp(logps) * logps).sum(-1) predicted_entropy = entropy_from_logits(logits, chunk_size=chunk_size) torch.testing.assert_close(predicted_entropy, entropy, rtol=1e-5, atol=1e-5) @require_rich class TestPrintPromptCompletionsSample(TrlTestCase): @patch("sys.stdout", new_callable=StringIO) def test_print_output(self, mock_stdout): prompts = ["The sky is", "The sun is"] completions = [" blue.", " in the sky."] rewards = {"Correctness": [0.123, 0.456], "Format": [0.789, 0.101]} advantages = [0.987, 0.654] step = 42 print_prompt_completions_sample(prompts, completions, rewards, advantages, step) output = mock_stdout.getvalue() # docstyle-ignore expected_output = textwrap.dedent("""\ ╭──────────────────────────── Step 42 ─────────────────────────────╮ │ ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Correctness ┃ Format ┃ Advantage ┃ │ │ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ │ │ │ The sky is │ blue. │ 0.12 │ 0.79 │ 0.99 │ │ │ ├────────────┼──────────────┼─────────────┼────────┼───────────┤ │ │ │ The sun is │ in the sky. │ 0.46 │ 0.10 │ 0.65 │ │ │ └────────────┴──────────────┴─────────────┴────────┴───────────┘ │ ╰──────────────────────────────────────────────────────────────────╯ """) assert output == expected_output @patch("sys.stdout", new_callable=StringIO) def test_num_samples(self, mock_stdout): prompts = ["A", "B"] completions = ["1", "2"] rewards = {"Score": [0.1, 0.2]} advantages = [0.3, 0.4] step = 10 print_prompt_completions_sample(prompts, completions, rewards, advantages, step, num_samples=1) output = mock_stdout.getvalue() # docstyle-ignore possible_outputs = [ textwrap.dedent("""\ ╭────────────────── Step 10 ──────────────────╮ │ ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Score ┃ Advantage ┃ │ │ ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━┩ │ │ │ A │ 1 │ 0.10 │ 0.30 │ │ │ └────────┴────────────┴───────┴───────────┘ │ ╰─────────────────────────────────────────────╯ """), # docstyle-ignore textwrap.dedent("""\ ╭────────────────── Step 10 ──────────────────╮ │ ┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Score ┃ Advantage ┃ │ │ ┡━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━┩ │ │ │ B │ 2 │ 0.20 │ 0.40 │ │ │ └────────┴────────────┴───────┴───────────┘ │ ╰─────────────────────────────────────────────╯ """), ] assert output in possible_outputs @patch("sys.stdout", new_callable=StringIO) def test_print_messages(self, mock_stdout): prompts = [ [ {"role": "system", "content": "You are an helpful assistant."}, {"role": "user", "content": "What color is the sky?"}, ], [ {"role": "system", "content": "You are an helpful assistant."}, {"role": "user", "content": "Where is the sun?"}, ], ] completions = [ [{"role": "assistant", "content": "It is blue."}], [{"role": "assistant", "content": "In the sky."}], ] rewards = {"Correctness": [0.123, 0.456], "Format": [0.789, 0.101]} advantages = [0.987, 0.654] step = 42 print_prompt_completions_sample(prompts, completions, rewards, advantages, step) output = mock_stdout.getvalue() # docstyle-ignore expected_output = textwrap.dedent("""\ ╭────────────────────────────────── Step 42 ───────────────────────────────────╮ │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Correctness ┃ Format ┃ Advantage ┃ │ │ ┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ │ │ │ SYSTEM │ ASSISTANT │ 0.12 │ 0.79 │ 0.99 │ │ │ │ You are an helpful │ It is blue. │ │ │ │ │ │ │ assistant. │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ USER │ │ │ │ │ │ │ │ What color is the sky? │ │ │ │ │ │ │ ├─────────────────────────┼─────────────┼─────────────┼────────┼───────────┤ │ │ │ SYSTEM │ ASSISTANT │ 0.46 │ 0.10 │ 0.65 │ │ │ │ You are an helpful │ In the sky. │ │ │ │ │ │ │ assistant. │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ USER │ │ │ │ │ │ │ │ Where is the sun? │ │ │ │ │ │ │ └─────────────────────────┴─────────────┴─────────────┴────────┴───────────┘ │ ╰──────────────────────────────────────────────────────────────────────────────╯ """) assert output == expected_output @patch("sys.stdout", new_callable=StringIO) def test_print_messages_with_tools(self, mock_stdout): prompts = [ [{"role": "user", "content": "What is the temperature in Paris?"}], [{"role": "user", "content": "What is the weather in London?"}], ] completions = [ [{"role": "tool", "name": "get_temperature", "args": {"location": "Paris"}}], [{"role": "tool", "name": "get_weather", "args": {"location": "London"}}], ] rewards = {"Correctness": [0.123, 0.456], "Format": [0.789, 0.101]} advantages = [0.987, 0.654] step = 42 print_prompt_completions_sample(prompts, completions, rewards, advantages, step) output = mock_stdout.getvalue() # docstyle-ignore expected_output = textwrap.dedent("""\ ╭────────────────────────────────── Step 42 ───────────────────────────────────╮ │ ┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Correctness ┃ Format ┃ Advantage ┃ │ │ ┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ │ │ │ USER │ TOOL │ 0.12 │ 0.79 │ 0.99 │ │ │ │ What is the │ get_temperature(… │ │ │ │ │ │ │ temperature in │ 'Paris'}) │ │ │ │ │ │ │ Paris? │ │ │ │ │ │ │ ├───────────────────┼───────────────────┼─────────────┼────────┼───────────┤ │ │ │ USER │ TOOL │ 0.46 │ 0.10 │ 0.65 │ │ │ │ What is the │ get_weather({'lo… │ │ │ │ │ │ │ weather in │ 'London'}) │ │ │ │ │ │ │ London? │ │ │ │ │ │ │ └───────────────────┴───────────────────┴─────────────┴────────┴───────────┘ │ ╰──────────────────────────────────────────────────────────────────────────────╯ """) assert output == expected_output class TestSelectiveLogSoftmax(TrlTestCase): @pytest.mark.parametrize("dtype", [torch.float64, torch.float32, torch.float16, torch.bfloat16]) def test_selective_log_softmax(self, dtype): """Test selective_log_softmax with logits of different dtypes""" vocab_size = 1024 batch_size = 4 seq_len = 32 input_ids = torch.randint(low=0, high=vocab_size, size=(batch_size, seq_len)) logits = torch.randn(batch_size, seq_len, vocab_size, dtype=dtype) expected_output = torch.gather(logits.log_softmax(-1), dim=-1, index=input_ids.unsqueeze(-1)).squeeze(-1) actual_output = selective_log_softmax(logits, input_ids) if dtype in [torch.float16, torch.bfloat16]: # half-precision dtypes fall back to an exact method assert torch.equal(actual_output, expected_output) else: torch.testing.assert_close(actual_output, expected_output, rtol=1e-5, atol=1e-5) @pytest.mark.parametrize("dtype", [torch.float64, torch.float32, torch.float16, torch.bfloat16]) @pytest.mark.parametrize("k", [1, 8]) def test_selective_log_softmax_multi_index(self, dtype, k): """Test selective_log_softmax with logits of different dtypes and index widths""" vocab_size = 1024 batch_size = 4 seq_len = 32 index = torch.randint(low=0, high=vocab_size, size=(batch_size, seq_len, k)) logits = torch.randn(batch_size, seq_len, vocab_size, dtype=dtype) expected_output = torch.gather(logits.log_softmax(-1), dim=-1, index=index) actual_output = selective_log_softmax(logits, index) assert actual_output.shape == (batch_size, seq_len, k) if dtype in [torch.float16, torch.bfloat16]: # half-precision dtypes fall back to an exact method assert torch.equal(actual_output, expected_output) else: torch.testing.assert_close(actual_output, expected_output, rtol=1e-5, atol=1e-5) class TestShuffleSequenceDict(TrlTestCase): def test_shuffle_preserves_shape(self): x = torch.arange(6).reshape(3, 2) y = torch.arange(3).reshape(3, 1) tensor_dict = {"x": x.clone(), "y": y.clone()} shuffled = shuffle_sequence_dict(tensor_dict) assert shuffled["x"].shape == x.shape assert shuffled["y"].shape == y.shape def test_shuffle_consistent_across_tensors(self): # Use known patterns to check alignment x = torch.tensor([[10, 11], [20, 21], [30, 31]]) y = torch.tensor([[1], [2], [3]]) tensor_dict = {"x": x.clone(), "y": y.clone()} shuffled = shuffle_sequence_dict(tensor_dict) # Build a reverse map from shuffled x rows to y values for i in range(3): x_row = shuffled["x"][i] y_val = shuffled["y"][i].item() if torch.equal(x_row, torch.tensor([10, 11])): assert y_val == 1 elif torch.equal(x_row, torch.tensor([20, 21])): assert y_val == 2 elif torch.equal(x_row, torch.tensor([30, 31])): assert y_val == 3 else: pytest.fail("Unexpected x row in shuffled output.") def test_none_tensor_remains_none(self): x = torch.arange(6).reshape(3, 2) tensor_dict = {"x": x.clone(), "y": None} shuffled = shuffle_sequence_dict(tensor_dict) assert shuffled["y"] is None assert shuffled["x"].shape == x.shape def test_shuffle_with_list(self): x = torch.tensor([[10, 11], [20, 21], [30, 31]]) y = ["a", "b", "c"] sequence_dict = {"x": x.clone(), "y": y} shuffled = shuffle_sequence_dict(sequence_dict) # Check that the list y is shuffled in the same order as x for i in range(3): x_row = shuffled["x"][i] y_val = shuffled["y"][i] if torch.equal(x_row, torch.tensor([10, 11])): assert y_val == "a" elif torch.equal(x_row, torch.tensor([20, 21])): assert y_val == "b" elif torch.equal(x_row, torch.tensor([30, 31])): assert y_val == "c" else: pytest.fail("Unexpected x row in shuffled output.") class TestSplitTensorDict(TrlTestCase): def test_split_equal_chunks(self): x = torch.arange(12).reshape(6, 2) y = torch.arange(6).reshape(6, 1) tensor_dict = {"x": x, "y": y} result = split_tensor_dict(tensor_dict, 3) expected_x_chunks = torch.chunk(x, 3, dim=0) expected_y_chunks = torch.chunk(y, 3, dim=0) assert len(result) == 3 for i in range(3): assert torch.equal(result[i]["x"], expected_x_chunks[i]) assert torch.equal(result[i]["y"], expected_y_chunks[i]) def test_with_none_tensor(self): x = torch.arange(12).reshape(6, 2) tensor_dict = {"x": x, "y": None} result = split_tensor_dict(tensor_dict, 2) expected_x_chunks = torch.chunk(x, 2, dim=0) assert len(result) == 2 for i in range(2): assert torch.equal(result[i]["x"], expected_x_chunks[i]) assert result[i]["y"] is None def test_with_scalar(self): x = torch.arange(12).reshape(6, 2) tensor_dict = {"x": x, "y": torch.tensor(1)} result = split_tensor_dict(tensor_dict, 2) expected_x_chunks = torch.chunk(x, 2, dim=0) assert len(result) == 2 for i in range(2): assert torch.equal(result[i]["x"], expected_x_chunks[i]) assert torch.equal(result[i]["y"], torch.tensor(1)) class TestSplitPixelValuesByGrid(TrlTestCase): def test_split_correctly_0(self): batch = { "image_grid_thw": torch.tensor([[1, 2, 2], [1, 2, 2]]), "num_images": [1, 1], "pixel_values": torch.arange(8 * 3).reshape(8, 3), # Shape: [8, 3] } result = split_pixel_values_by_grid(batch) assert isinstance(result["pixel_values"], list) assert len(result["pixel_values"]) == 2 assert torch.equal(result["pixel_values"][0], batch["pixel_values"][:4]) assert torch.equal(result["pixel_values"][1], batch["pixel_values"][4:]) assert isinstance(result["image_grid_thw"], list) assert len(result["image_grid_thw"]) == 2 assert torch.equal(result["image_grid_thw"][0], torch.tensor([[1, 2, 2]])) assert torch.equal(result["image_grid_thw"][1], torch.tensor([[1, 2, 2]])) def test_split_correctly_1(self): batch = { "image_grid_thw": torch.tensor([[1, 2, 2], [1, 2, 4]]), "num_images": [1, 1], "pixel_values": torch.arange(12 * 3).reshape(12, 3), # Shape: [12, 3] } result = split_pixel_values_by_grid(batch) assert isinstance(result["pixel_values"], list) assert len(result["pixel_values"]) == 2 assert torch.equal(result["pixel_values"][0], batch["pixel_values"][:4]) assert torch.equal(result["pixel_values"][1], batch["pixel_values"][4:12]) assert isinstance(result["image_grid_thw"], list) assert len(result["image_grid_thw"]) == 2 assert torch.equal(result["image_grid_thw"][0], torch.tensor([[1, 2, 2]])) assert torch.equal(result["image_grid_thw"][1], torch.tensor([[1, 2, 4]])) def test_missing_keys(self): batch = {"pixel_values": torch.tensor([1.0])} result = split_pixel_values_by_grid(batch) assert result == batch def test_mismatched_length(self): batch = { "image_grid_thw": torch.tensor([[1, 1, 2], [1, 2, 1]]), # Total = 8 "num_images": [1, 1], "pixel_values": torch.randn(3, 5), # Only 3 rows } with pytest.raises(ValueError): split_pixel_values_by_grid(batch) def test_multi_images(self): batch = { "image_grid_thw": torch.tensor([[1, 1, 2], [1, 2, 2], [1, 2, 1]]), # Total = 8 "num_images": [1, 2], "pixel_values": torch.arange(8 * 3).reshape(8, 3), # Shape: [8, 3] } result = split_pixel_values_by_grid(batch) assert isinstance(result["pixel_values"], list) assert len(result["pixel_values"]) == 2 assert torch.equal(result["pixel_values"][0], batch["pixel_values"][:2]) assert torch.equal(result["pixel_values"][1], batch["pixel_values"][2:]) assert isinstance(result["image_grid_thw"], list) assert len(result["image_grid_thw"]) == 2 assert torch.equal(result["image_grid_thw"][0], torch.tensor([[1, 1, 2]])) assert torch.equal(result["image_grid_thw"][1], torch.tensor([[1, 2, 2], [1, 2, 1]])) class TestUnsplitPixelValuesByGrid(TrlTestCase): def test_unsplit_correctly(self): pixel_values = [torch.randn(4, 5), torch.randn(2, 5)] pixel_values_merged = torch.cat(pixel_values, dim=0) image_grid_thw = [torch.tensor([[1, 2, 2]]), torch.tensor([[1, 2, 1]])] image_grid_thw_merged = torch.cat(image_grid_thw, dim=0) batch = {"pixel_values": pixel_values, "image_grid_thw": image_grid_thw, "other_key": torch.tensor([1])} result = unsplit_pixel_values_by_grid(batch) assert isinstance(result["pixel_values"], torch.Tensor) torch.testing.assert_close(result["pixel_values"], pixel_values_merged) assert isinstance(result["image_grid_thw"], torch.Tensor) assert torch.equal(result["image_grid_thw"], image_grid_thw_merged) assert "other_key" in result def test_no_op_if_not_list(self): original = torch.randn(5, 3) batch = {"pixel_values": original} result = unsplit_pixel_values_by_grid(batch) assert torch.equal(result["pixel_values"], original) class TestForwardMaskedLogits: @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-CohereForCausalLM", "trl-internal-testing/tiny-Cohere2ForCausalLM", "trl-internal-testing/tiny-DeepseekV3ForCausalLM", "trl-internal-testing/tiny-DeepseekV3ForCausalLM-0528", "trl-internal-testing/tiny-Gemma2ForCausalLM", "trl-internal-testing/tiny-GemmaForCausalLM", "trl-internal-testing/tiny-Glm4MoeForCausalLM", "trl-internal-testing/tiny-GptOssForCausalLM", "trl-internal-testing/tiny-LlamaForCausalLM-3.1", "trl-internal-testing/tiny-LlamaForCausalLM-3.2", "trl-internal-testing/tiny-LlamaForCausalLM-3", "trl-internal-testing/tiny-MistralForCausalLM-0.1", "trl-internal-testing/tiny-MistralForCausalLM-0.2", "trl-internal-testing/tiny-Phi3ForCausalLM", "trl-internal-testing/tiny-Qwen2ForCausalLM-2.5", "trl-internal-testing/tiny-Qwen3ForCausalLM", ], ) def test_llm(self, model_id): device = torch.device("cuda") model = AutoModelForCausalLM.from_pretrained(model_id, dtype="auto", device_map=device) input_ids = torch.randint(0, model.config.vocab_size, (2, 8), device=device) logits_mask = torch.tensor( [[1, 1, 0, 0, 1, 0, 1, 0], [0, 1, 1, 0, 0, 1, 0, 1]], device=device, ) full_outputs = model(input_ids=input_ids) masked_outputs = forward_masked_logits(model, logits_mask, input_ids=input_ids) torch.testing.assert_close( masked_outputs.flat_logits, full_outputs.logits[logits_mask.bool()], ) @pytest.mark.parametrize( "model_id", [ "trl-internal-testing/tiny-Gemma3ForConditionalGeneration", "trl-internal-testing/tiny-Idefics2ForConditionalGeneration", "trl-internal-testing/tiny-Idefics3ForConditionalGeneration", "trl-internal-testing/tiny-LlavaForConditionalGeneration", "trl-internal-testing/tiny-LlavaNextForConditionalGeneration", "trl-internal-testing/tiny-Qwen2VLForConditionalGeneration", "trl-internal-testing/tiny-Qwen2_5_VLForConditionalGeneration", # "trl-internal-testing/tiny-SmolVLMForConditionalGeneration", seems not to support bf16 properly pytest.param( "trl-internal-testing/tiny-Qwen3VLForConditionalGeneration", marks=[ pytest.mark.skipif( Version(transformers.__version__) < Version("4.57.0"), reason="Qwen3-VL series were introduced in transformers-4.57.0", ), pytest.mark.xfail( Version("5.0.0") <= Version(transformers.__version__) < Version("5.1.0"), reason="Upstream transformers bug (transformers#43334) in 5.0.x; fixed in 5.1.0", ), ], ), pytest.param( "trl-internal-testing/tiny-Qwen3_5ForConditionalGeneration", marks=pytest.mark.skipif( Version(transformers.__version__) < Version("5.2.0"), reason="Qwen3.5 models were introduced in transformers-5.2.0", ), ), ], ) def test_vlm(self, model_id): device = torch.device("cuda") model = AutoModelForImageTextToText.from_pretrained(model_id, dtype="auto", device_map=device) input_ids = torch.randint(0, model.config.text_config.vocab_size, (2, 8), device=device) logits_mask = torch.tensor( [[1, 1, 0, 0, 1, 0, 1, 0], [0, 1, 1, 0, 0, 1, 0, 1]], device=device, ) full_outputs = model(input_ids=input_ids) masked_outputs = forward_masked_logits(model, logits_mask, input_ids=input_ids) torch.testing.assert_close( masked_outputs.flat_logits, full_outputs.logits[logits_mask.bool()], ) ================================================ FILE: tests/test_vllm_client_server.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import subprocess from types import SimpleNamespace import pytest from packaging.version import Version from transformers import AutoModelForCausalLM, AutoProcessor, AutoTokenizer from transformers.testing_utils import torch_device from trl.generation.vllm_client import VLLMClient from trl.generation.vllm_generation import extract_logprobs from trl.import_utils import is_vllm_available from trl.scripts.vllm_serve import chunk_list from .testing_utils import ( TrlTestCase, kill_process, require_3_accelerators, require_torch_multi_accelerator, require_vision, require_vllm, ) if is_vllm_available(): import vllm from vllm import LLM, SamplingParams _is_vllm_ge_014 = Version(vllm.__version__) >= Version("0.14.0") else: _is_vllm_ge_014 = False class TestChunkList(TrlTestCase): def test_even_split(self): assert chunk_list([1, 2, 3, 4, 5, 6], 2) == [[1, 2, 3], [4, 5, 6]] def test_uneven_split(self): assert chunk_list([1, 2, 3, 4, 5, 6], 4) == [[1, 2], [3, 4], [5], [6]] def test_more_chunks_than_elements(self): assert chunk_list([1, 2, 3, 4, 5, 6], 8) == [[1], [2], [3], [4], [5], [6], [], []] def test_n_equals_len(self): assert chunk_list([1, 2, 3], 3) == [[1], [2], [3]] def test_n_is_1(self): assert chunk_list([1, 2, 3], 1) == [[1, 2, 3]] def test_single_element_list(self): assert chunk_list([42], 2) == [[42], []] def test_any_dtype(self): assert chunk_list([1, "two", 3.0, {"four": 4}, ["f", "i", "v", "e"]], 2) == [ [1, "two", 3.0], [{"four": 4}, ["f", "i", "v", "e"]], ] class TestExtractLogprobs(TrlTestCase): def test_extract_logprobs_sorts_by_rank_and_replaces_nan(self): all_outputs = [ SimpleNamespace( outputs=[ SimpleNamespace( logprobs=[ { 11: SimpleNamespace(rank=1, logprob=-0.2), 99: SimpleNamespace(rank=0, logprob=-0.1), 42: SimpleNamespace(rank=2, logprob=float("nan")), }, { 5: SimpleNamespace(rank=0, logprob=-1.1), }, ] ) ] ), SimpleNamespace( outputs=[ SimpleNamespace( logprobs=[ { 3: SimpleNamespace(rank=1, logprob=-0.5), 7: SimpleNamespace(rank=0, logprob=-0.4), } ] ) ] ), ] all_logprobs, all_token_ids = extract_logprobs(all_outputs) assert all_token_ids == [ [[99, 11, 42], [5]], [[7, 3]], ] assert all_logprobs == [ [[-0.1, -0.2, None], [-1.1]], [[-0.4, -0.5]], ] def test_extract_logprobs_returns_none_token_ids_when_logprobs_missing(self): all_outputs = [SimpleNamespace(outputs=[SimpleNamespace(logprobs=None)])] all_logprobs, all_token_ids = extract_logprobs(all_outputs) assert all_logprobs is None assert all_token_ids is None @pytest.mark.slow @require_torch_multi_accelerator @require_vllm class TestVLLMClientServer(TrlTestCase): model_id = "Qwen/Qwen2.5-1.5B" @classmethod def setup_class(cls): # We want the server to run on accelerator 1, so we set VISIBLE_DEVICES to "1" env = os.environ.copy() VISIBLE_DEVICES = "ZE_AFFINITY_MASK" if torch_device == "xpu" else "CUDA_VISIBLE_DEVICES" env[VISIBLE_DEVICES] = "1" # Restrict to accelerator 1 # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env ) # Initialize the client cls.client = VLLMClient(connection_timeout=240, host="localhost") cls.client.init_communicator() def test_generate(self): prompts = ["Hello, AI!", "Tell me a joke"] outputs = self.client.generate(prompts) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_logprobs_none(self): outputs = self.client.generate(["Hello, AI!"], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat(self): messages = [[{"role": "user", "content": "Hello, AI!"}], [{"role": "user", "content": "Tell me a joke"}]] outputs = self.client.chat(messages) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of messages assert len(prompt_ids) == len(messages) assert len(completion_ids) == len(messages) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_chat_with_logprobs_none(self): outputs = self.client.chat([[{"role": "user", "content": "Hello, AI!"}]], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat_with_tools(self): def multiply(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b messages = [[{"role": "user", "content": "What is 3 multiplied by 4?"}]] outputs = self.client.chat(messages, tools=[multiply]) # Decode prompt and check that "Multiplies two integers." is in the prompt. tokenizer = AutoTokenizer.from_pretrained(self.model_id) decoded_prompt = tokenizer.decode(outputs["prompt_ids"][0]) assert "Multiplies two integers." in decoded_prompt def test_generate_with_token_ids(self): tokenizer = AutoTokenizer.from_pretrained(self.model_id) prompts = ["Hello, AI!", "Tell me a joke"] prompt_token_ids = tokenizer(prompts)["input_ids"] outputs = self.client.generate(prompt_token_ids) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that prompt_ids match the input token IDs assert prompt_ids == prompt_token_ids # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_params(self): prompts = ["Hello, AI!", "Tell me a joke"] completion_ids = self.client.generate(prompts, n=2, repetition_penalty=0.9, temperature=0.8, max_tokens=32)[ "completion_ids" ] # Check that the output is a list assert isinstance(completion_ids, list) # Check that the number of generated sequences is 2 times the number of prompts assert len(completion_ids) == 2 * len(prompts) # Check that the generated sequences are lists of integers for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) # Check that the length of the generated sequences is less than or equal to 32 for seq in completion_ids: assert len(seq) <= 32 def test_update_model_params(self): model = AutoModelForCausalLM.from_pretrained(self.model_id, device_map=torch_device) self.client.update_model_params(model) def test_reset_prefix_cache(self): # Test resetting the prefix cache self.client.reset_prefix_cache() @pytest.mark.xfail(reason="Importing `bitsandbytes` causes issues, see vllm-project/vllm#32793") def test_logprobs_match_with_non_default_sampling(self): prompts = ["Hello, AI!", "Tell me a joke"] # Use non-default sampling parameters (especially temperature) to ensure vLLM applies logprob processing. With # default sampling, raw and processed logprobs are identical, so mismatches would not be detected. temperature = 0.7 repetition_penalty = 1.05 top_p = 0.9 max_tokens = 8 seed = 1234 num_logprobs = 5 server_outputs = self.client.generate( prompts, temperature=temperature, repetition_penalty=repetition_penalty, top_p=top_p, max_tokens=max_tokens, logprobs=num_logprobs, generation_kwargs={"seed": seed}, ) os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" llm = LLM( model=self.model_id, tensor_parallel_size=1, gpu_memory_utilization=0.2, max_model_len=128, logprobs_mode="processed_logprobs", ) sampling_params = SamplingParams( temperature=temperature, repetition_penalty=repetition_penalty, top_p=top_p, max_tokens=max_tokens, logprobs=num_logprobs, seed=seed, ) colocate_outputs = llm.generate(prompts, sampling_params=sampling_params, use_tqdm=False) colocate_prompt_ids = [output.prompt_token_ids for output in colocate_outputs] colocate_completion_ids = [ list(output.token_ids) for outputs in colocate_outputs for output in outputs.outputs ] colocate_logprobs, colocate_logprob_token_ids = extract_logprobs(colocate_outputs) # Generation correctness: prompt and completion IDs match between server and colocate assert server_outputs["prompt_ids"] == colocate_prompt_ids assert server_outputs["completion_ids"] == colocate_completion_ids server_logprobs = server_outputs["logprobs"] server_logprob_token_ids = server_outputs["logprob_token_ids"] # Shape: both should be (num_sequences, seq_len, num_logprobs) with multiple logprobs per token assert len(server_logprobs) == len(prompts) assert len(server_logprob_token_ids) == len(prompts) for seq_lps in server_logprobs: for token_lps in seq_lps: assert len(token_lps) > 1, "Expected multiple logprobs per token when logprobs > 0" # Value correctness: server extraction matches colocate extraction via extract_logprobs assert server_logprob_token_ids == colocate_logprob_token_ids for server_seq, colocate_seq in zip(server_logprobs, colocate_logprobs, strict=True): assert len(server_seq) == len(colocate_seq) for server_token_lps, colocate_token_lps in zip(server_seq, colocate_seq, strict=True): assert server_token_lps == pytest.approx(colocate_token_lps, rel=1e-6, abs=1e-6) # Ordering: logprobs at each position should be sorted descending for seq_lps in server_logprobs: for token_lps in seq_lps: assert token_lps == sorted(token_lps, reverse=True), "Logprobs should be sorted descending" # Sampled token presence: the actual completion token should appear in the logprob token IDs for seq_idx, (completion_seq, token_ids_seq) in enumerate( zip(server_outputs["completion_ids"], server_logprob_token_ids, strict=True) ): for pos, (sampled_id, lp_ids) in enumerate(zip(completion_seq, token_ids_seq, strict=True)): assert sampled_id in lp_ids, ( f"Sampled token {sampled_id} not found in logprob token IDs {lp_ids} " f"at sequence {seq_idx}, position {pos}" ) @classmethod def teardown_class(cls): # Close the client cls.client.close_communicator() # vLLM x pytest (or Popen) seems not to handle process termination well. To avoid zombie processes, we need to # kill the server process and its children explicitly. kill_process(cls.server_process) # Same as above but using base_url to instantiate the client. @pytest.mark.slow @require_torch_multi_accelerator @require_vllm class TestVLLMClientServerBaseURL(TrlTestCase): model_id = "Qwen/Qwen2.5-1.5B" @classmethod def setup_class(cls): # We want the server to run on accelerator 1, so we set VISIBLE_DEVICES to "1" env = os.environ.copy() VISIBLE_DEVICES = "ZE_AFFINITY_MASK" if torch_device == "xpu" else "CUDA_VISIBLE_DEVICES" env[VISIBLE_DEVICES] = "1" # Restrict to accelerator 1 # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env ) # Initialize the client cls.client = VLLMClient(base_url="http://localhost:8000", connection_timeout=240) cls.client.init_communicator() def test_generate(self): prompts = ["Hello, AI!", "Tell me a joke"] outputs = self.client.generate(prompts) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_logprobs_none(self): outputs = self.client.generate(["Hello, AI!"], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat(self): messages = [[{"role": "user", "content": "Hello, AI!"}], [{"role": "user", "content": "Tell me a joke"}]] outputs = self.client.chat(messages) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of messages assert len(prompt_ids) == len(messages) assert len(completion_ids) == len(messages) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_chat_with_logprobs_none(self): outputs = self.client.chat([[{"role": "user", "content": "Hello, AI!"}]], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat_with_tools(self): def multiply(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b messages = [[{"role": "user", "content": "What is 3 multiplied by 4?"}]] outputs = self.client.chat(messages, tools=[multiply]) # Decode prompt and check that "Multiplies two integers." is in the prompt. tokenizer = AutoTokenizer.from_pretrained(self.model_id) decoded_prompt = tokenizer.decode(outputs["prompt_ids"][0]) assert "Multiplies two integers." in decoded_prompt def test_generate_with_token_ids(self): tokenizer = AutoTokenizer.from_pretrained(self.model_id) prompts = ["Hello, AI!", "Tell me a joke"] prompt_token_ids = tokenizer(prompts)["input_ids"] outputs = self.client.generate(prompt_token_ids) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that prompt_ids match the input token IDs assert prompt_ids == prompt_token_ids # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_params(self): prompts = ["Hello, AI!", "Tell me a joke"] completion_ids = self.client.generate(prompts, n=2, repetition_penalty=0.9, temperature=0.8, max_tokens=32)[ "completion_ids" ] # Check that the output is a list assert isinstance(completion_ids, list) # Check that the number of generated sequences is 2 times the number of prompts assert len(completion_ids) == 2 * len(prompts) # Check that the generated sequences are lists of integers for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) # Check that the length of the generated sequences is less than or equal to 32 for seq in completion_ids: assert len(seq) <= 32 def test_update_model_params(self): model = AutoModelForCausalLM.from_pretrained(self.model_id, device_map=torch_device) self.client.update_model_params(model) def test_reset_prefix_cache(self): # Test resetting the prefix cache self.client.reset_prefix_cache() @classmethod def teardown_class(cls): # Close the client cls.client.close_communicator() # vLLM x pytest (or Popen) seems not to handle process termination well. To avoid zombie processes, we need to # kill the server process and its children explicitly. kill_process(cls.server_process) @pytest.mark.slow @require_3_accelerators @require_vllm class TestVLLMClientServerTP(TrlTestCase): model_id = "Qwen/Qwen2.5-1.5B" @classmethod def setup_class(cls): # We want the server to run on accelerator 1 and 2, so we set VISIBLE_DEVICES to "1,2" env = os.environ.copy() VISIBLE_DEVICES = "ZE_AFFINITY_MASK" if torch_device == "xpu" else "CUDA_VISIBLE_DEVICES" env[VISIBLE_DEVICES] = "1,2" # Restrict to accelerator 1 and 2 # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id, "--tensor_parallel_size", "2"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, ) # Initialize the client cls.client = VLLMClient(connection_timeout=240, host="localhost") cls.client.init_communicator() def test_generate(self): prompts = ["Hello, AI!", "Tell me a joke"] outputs = self.client.generate(prompts) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_logprobs_none(self): outputs = self.client.generate(["Hello, AI!"], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat(self): messages = [[{"role": "user", "content": "Hello, AI!"}], [{"role": "user", "content": "Tell me a joke"}]] outputs = self.client.chat(messages) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of messages assert len(prompt_ids) == len(messages) assert len(completion_ids) == len(messages) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_chat_with_logprobs_none(self): outputs = self.client.chat([[{"role": "user", "content": "Hello, AI!"}]], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat_with_tools(self): def multiply(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b messages = [[{"role": "user", "content": "What is 3 multiplied by 4?"}]] outputs = self.client.chat(messages, tools=[multiply]) # Decode prompt and check that "Multiplies two integers." is in the prompt. tokenizer = AutoTokenizer.from_pretrained(self.model_id) decoded_prompt = tokenizer.decode(outputs["prompt_ids"][0]) assert "Multiplies two integers." in decoded_prompt def test_generate_with_token_ids(self): tokenizer = AutoTokenizer.from_pretrained(self.model_id) prompts = ["Hello, AI!", "Tell me a joke"] prompt_token_ids = tokenizer(prompts)["input_ids"] outputs = self.client.generate(prompt_token_ids) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that prompt_ids match the input token IDs assert prompt_ids == prompt_token_ids # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_params(self): prompts = ["Hello, AI!", "Tell me a joke"] completion_ids = self.client.generate(prompts, n=2, repetition_penalty=0.9, temperature=0.8, max_tokens=32)[ "completion_ids" ] # Check that the output is a list assert isinstance(completion_ids, list) # Check that the number of generated sequences is 2 times the number of prompts assert len(completion_ids) == 2 * len(prompts) # Check that the generated sequences are lists of integers for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) # Check that the length of the generated sequences is less than or equal to 32 for seq in completion_ids: assert len(seq) <= 32 def test_update_model_params(self): model = AutoModelForCausalLM.from_pretrained(self.model_id, device_map=torch_device) self.client.update_model_params(model) def test_reset_prefix_cache(self): # Test resetting the prefix cache self.client.reset_prefix_cache() @classmethod def teardown_class(cls): # Close the client cls.client.close_communicator() # vLLM x pytest (or Popen) seems not to handle process termination well. To avoid zombie processes, we need to # kill the server process and its children explicitly. kill_process(cls.server_process) @pytest.mark.slow @pytest.mark.skipif( _is_vllm_ge_014, reason="Skipping DP server test for vLLM>=0.14.0 (PR vllm#30739: DP for non-MoE/dense models no longer supported).", ) @require_3_accelerators @require_vllm class TestVLLMClientServerDP(TrlTestCase): model_id = "Qwen/Qwen2.5-1.5B" @classmethod def setup_class(cls): # We want the server to run on accelerator 1 and 2, so we set VISIBLE_DEVICES to "1,2" env = os.environ.copy() VISIBLE_DEVICES = "ZE_AFFINITY_MASK" if torch_device == "xpu" else "CUDA_VISIBLE_DEVICES" env[VISIBLE_DEVICES] = "1,2" # Restrict to accelerator 1 and 2 # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id, "--data_parallel_size", "2"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, ) # Initialize the client cls.client = VLLMClient(connection_timeout=240, host="localhost") cls.client.init_communicator() def test_generate(self): prompts = ["Hello, AI!", "Tell me a joke"] outputs = self.client.generate(prompts) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_logprobs_none(self): outputs = self.client.generate(["Hello, AI!"], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat(self): messages = [[{"role": "user", "content": "Hello, AI!"}], [{"role": "user", "content": "Tell me a joke"}]] outputs = self.client.chat(messages) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of messages assert len(prompt_ids) == len(messages) assert len(completion_ids) == len(messages) # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_chat_with_logprobs_none(self): outputs = self.client.chat([[{"role": "user", "content": "Hello, AI!"}]], logprobs=None) assert isinstance(outputs["prompt_ids"], list) assert isinstance(outputs["completion_ids"], list) assert outputs["logprobs"] is None assert outputs["logprob_token_ids"] is None def test_chat_with_tools(self): def multiply(a: int, b: int) -> int: """ Multiplies two integers. Args: a: The first integer. b: The second integer. Returns: The product of the two integers. """ return a * b messages = [[{"role": "user", "content": "What is 3 multiplied by 4?"}]] outputs = self.client.chat(messages, tools=[multiply]) # Decode prompt and check that "Multiplies two integers." is in the prompt. tokenizer = AutoTokenizer.from_pretrained(self.model_id) decoded_prompt = tokenizer.decode(outputs["prompt_ids"][0]) assert "Multiplies two integers." in decoded_prompt def test_generate_with_token_ids(self): tokenizer = AutoTokenizer.from_pretrained(self.model_id) prompts = ["Hello, AI!", "Tell me a joke"] prompt_token_ids = tokenizer(prompts)["input_ids"] outputs = self.client.generate(prompt_token_ids) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] # Check that the outputs are lists assert isinstance(prompt_ids, list) assert isinstance(completion_ids, list) # Check that the number of sequences are equal to the number of prompts assert len(prompt_ids) == len(prompts) assert len(completion_ids) == len(prompts) # Check that prompt_ids match the input token IDs assert prompt_ids == prompt_token_ids # Check that the sequences are lists of integers for seq in prompt_ids: assert all(isinstance(tok, int) for tok in seq) for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) def test_generate_with_params(self): prompts = ["Hello, AI!", "Tell me a joke"] completion_ids = self.client.generate(prompts, n=2, repetition_penalty=0.9, temperature=0.8, max_tokens=32)[ "completion_ids" ] # Check that the output is a list assert isinstance(completion_ids, list) # Check that the number of generated sequences is 2 times the number of prompts assert len(completion_ids) == 2 * len(prompts) # Check that the generated sequences are lists of integers for seq in completion_ids: assert all(isinstance(tok, int) for tok in seq) # Check that the length of the generated sequences is less than or equal to 32 for seq in completion_ids: assert len(seq) <= 32 def test_update_model_params(self): model = AutoModelForCausalLM.from_pretrained(self.model_id, device_map=torch_device) self.client.update_model_params(model) def test_reset_prefix_cache(self): # Test resetting the prefix cache self.client.reset_prefix_cache() @classmethod def teardown_class(cls): # Close the client cls.client.close_communicator() # vLLM x pytest (or Popen) seems not to handle process termination well. To avoid zombie processes, we need to # kill the server process and its children explicitly. kill_process(cls.server_process) @pytest.mark.slow @require_torch_multi_accelerator @require_vllm class TestVLLMClientServerDeviceParameter(TrlTestCase): """Test the device parameter functionality in init_communicator.""" model_id = "Qwen/Qwen2.5-1.5B" @classmethod def setup_class(cls): # We want the server to run on accelerator 1, so we set VISIBLE_DEVICES to "1" env = os.environ.copy() VISIBLE_DEVICES = "ZE_AFFINITY_MASK" if torch_device == "xpu" else "CUDA_VISIBLE_DEVICES" env[VISIBLE_DEVICES] = "1" # Restrict to accelerator 1 # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env ) def test_init_communicator_with_device_int(self): """Test init_communicator with integer device parameter.""" client = VLLMClient(connection_timeout=240, host="localhost") client.init_communicator(device=0) # Explicitly specify device 0 # Test basic functionality prompts = ["Hello, AI!"] outputs = client.generate(prompts) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] assert isinstance(prompt_ids, list) assert len(prompt_ids) == len(prompts) assert isinstance(completion_ids, list) assert len(completion_ids) == len(prompts) client.close_communicator() def test_init_communicator_with_device_string(self): """Test init_communicator with string device parameter.""" client = VLLMClient(connection_timeout=240, host="localhost") client.init_communicator(device=0) # Explicitly specify device as string # Test basic functionality prompts = ["Hello, AI!"] outputs = client.generate(prompts)["completion_ids"] assert isinstance(outputs, list) assert len(outputs) == len(prompts) client.close_communicator() def test_init_communicator_with_torch_device(self): """Test init_communicator with torch.device object.""" import torch client = VLLMClient(connection_timeout=240, host="localhost") device = torch.device(0) client.init_communicator(device=device) # Explicitly specify torch.device object # Test basic functionality prompts = ["Hello, AI!"] outputs = client.generate(prompts)["completion_ids"] assert isinstance(outputs, list) assert len(outputs) == len(prompts) client.close_communicator() @classmethod def teardown_class(cls): # vLLM x pytest (or Popen) seems not to handle process termination well. To avoid zombie processes, we need to # kill the server process and its children explicitly. kill_process(cls.server_process) @pytest.mark.slow @require_vllm @require_vision class TestVLLMClientServerVLM(TrlTestCase): model_id = "Qwen/Qwen2.5-VL-3B-Instruct" @classmethod def setup_class(cls): # Start the server process cls.server_process = subprocess.Popen( ["trl", "vllm-serve", "--model", cls.model_id], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) # Initialize the client (no communicator needed for generation-only tests) cls.client = VLLMClient(connection_timeout=240, host="localhost") def test_generate_with_token_ids_and_image(self): from PIL import Image processor = AutoProcessor.from_pretrained(self.model_id) image1 = Image.new("RGB", (64, 64), color="red") image2 = Image.new("RGB", (64, 64), color="blue") image3 = Image.new("RGB", (64, 64), color="green") messages = [ [ { "role": "user", "content": [ {"type": "image", "image": image1}, {"type": "image", "image": image2}, {"type": "text", "text": "What are the differences between these two images?"}, ], } ], [ { "role": "user", "content": [ {"type": "image", "image": image3}, {"type": "text", "text": "What is the color of this image?"}, ], } ], ] prompt_token_ids = processor.apply_chat_template( conversation=messages, tokenize=True, add_generation_prompt=True ) outputs = self.client.generate(prompt_token_ids, images=[[image1, image2], [image3]], max_tokens=64) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] assert len(prompt_ids) == 2 assert len(completion_ids) == 2 assert all(isinstance(tok, int) for tok in prompt_ids[0]) assert all(isinstance(tok, int) for tok in completion_ids[0]) def test_generate_with_token_ids_mixed_images(self): """Test a batch where one prompt has an image and the other does not.""" from PIL import Image processor = AutoProcessor.from_pretrained(self.model_id) image = Image.new("RGB", (64, 64), color="red") messages = [ [ { "role": "user", "content": [{"type": "image", "image": image}, {"type": "text", "text": "Describe this image."}], } ], [ { "role": "user", "content": [{"type": "text", "text": "What is 1+1?"}], } ], ] prompt_token_ids = processor.apply_chat_template( conversation=messages, tokenize=True, add_generation_prompt=True ) outputs = self.client.generate(prompt_token_ids, images=[[image], None], max_tokens=64) prompt_ids = outputs["prompt_ids"] completion_ids = outputs["completion_ids"] assert len(prompt_ids) == 2 assert len(completion_ids) == 2 assert all(isinstance(tok, int) for tok in prompt_ids[0]) assert all(isinstance(tok, int) for tok in prompt_ids[1]) assert all(isinstance(tok, int) for tok in completion_ids[0]) assert all(isinstance(tok, int) for tok in completion_ids[1]) @classmethod def teardown_class(cls): kill_process(cls.server_process) ================================================ FILE: tests/testing_constants.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. CI_HUB_USER = "__DUMMY_TRANSFORMERS_USER__" CI_HUB_USER_FULL_NAME = "Dummy User" CI_HUB_ENDPOINT = "https://hub-ci.huggingface.co" ================================================ FILE: tests/testing_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import signal import warnings from collections.abc import Callable import psutil import pytest import torch from transformers import is_bitsandbytes_available, is_comet_available, is_sklearn_available, is_wandb_available from transformers.testing_utils import backend_device_count, torch_device from transformers.utils import ( is_kernels_available, is_peft_available, is_rich_available, is_torch_available, is_vision_available, ) from trl.import_utils import ( is_jmespath_available, is_joblib_available, is_liger_kernel_available, is_llm_blender_available, is_math_verify_available, is_mergekit_available, is_vllm_available, ) require_bitsandbytes = pytest.mark.skipif(not is_bitsandbytes_available(), reason="test requires bitsandbytes") require_comet = pytest.mark.skipif(not is_comet_available(), reason="test requires comet_ml") require_jmespath = pytest.mark.skipif(not is_jmespath_available(), reason="test requires jmespath") require_kernels = pytest.mark.skipif(not is_kernels_available(), reason="test requires kernels") require_liger_kernel = pytest.mark.skipif(not is_liger_kernel_available(), reason="test requires liger-kernel") require_llm_blender = pytest.mark.skipif(not is_llm_blender_available(), reason="test requires llm-blender") require_math_latex = pytest.mark.skipif(not is_math_verify_available(), reason="test requires math_verify") require_mergekit = pytest.mark.skipif(not is_mergekit_available(), reason="test requires mergekit") require_peft = pytest.mark.skipif(not is_peft_available(), reason="test requires peft") require_rich = pytest.mark.skipif(not is_rich_available(), reason="test requires rich") require_sklearn = pytest.mark.skipif( not (is_sklearn_available() and is_joblib_available()), reason="test requires sklearn" ) require_torch_accelerator = pytest.mark.skipif( torch_device is None or torch_device == "cpu", reason="test requires accelerator" ) require_torch_multi_accelerator = pytest.mark.skipif( not is_torch_available() or backend_device_count(torch_device) <= 1, reason="test requires multiple accelerators" ) require_vision = pytest.mark.skipif(not is_vision_available(), reason="test requires vision") require_vllm = pytest.mark.skipif(not is_vllm_available(), reason="test requires vllm") require_wandb = pytest.mark.skipif(not is_wandb_available(), reason="test requires wandb") require_no_wandb = pytest.mark.skipif(is_wandb_available(), reason="test requires no wandb") require_3_accelerators = pytest.mark.skipif( not (getattr(torch, torch_device, torch.cuda).device_count() >= 3), reason=f"test requires at least 3 {torch_device}s", ) def is_bitsandbytes_multi_backend_available() -> bool: if is_bitsandbytes_available(): import bitsandbytes as bnb return "multi_backend" in getattr(bnb, "features", set()) return False # Function ported from transformers.testing_utils before transformers#41283 require_torch_gpu_if_bnb_not_multi_backend_enabled = pytest.mark.skipif( not is_bitsandbytes_multi_backend_available() and not torch_device == "cuda", reason="test requires bitsandbytes multi-backend enabled or 'cuda' torch device", ) def is_ampere_or_newer(device_index=0): if not torch.cuda.is_available(): return False major, minor = torch.cuda.get_device_capability(device_index) # Ampere starts at compute capability 8.0 (e.g., A100 = 8.0, RTX 30xx = 8.6) return (major, minor) >= (8, 0) require_ampere_or_newer = pytest.mark.skipif(not is_ampere_or_newer(), reason="test requires Ampere or newer GPU") class TrlTestCase: @pytest.fixture(autouse=True) def set_tmp_dir(self, tmp_path): self.tmp_dir = str(tmp_path) def ignore_warnings(message: str = None, category: type[Warning] = Warning) -> Callable: """ Decorator to ignore warnings with a specific message and/or category. Args: message (`str`, *optional*): Regex pattern for the warning message to ignore. If `None`, all messages are ignored. category (`type[Warning]`, *optional*, defaults to `Warning`): Warning class to ignore. Defaults to `Warning`, which ignores all warnings. """ def decorator(test_func): @functools.wraps(test_func) def wrapper(*args, **kwargs): with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=message, category=category) return test_func(*args, **kwargs) return wrapper return decorator def kill_process(process): parent = psutil.Process(process.pid) children = parent.children(recursive=True) for child in children: try: child.send_signal(signal.SIGTERM) child.wait(timeout=5) except psutil.TimeoutExpired: child.kill() except psutil.NoSuchProcess: pass try: process.terminate() process.wait(timeout=5) except psutil.TimeoutExpired: process.kill() except psutil.NoSuchProcess: pass ================================================ FILE: trl/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from importlib.metadata import PackageNotFoundError, version from typing import TYPE_CHECKING from . import _compat from ._lazy_module import _LazyModule try: __version__ = version("trl") except PackageNotFoundError: __version__ = "unknown" _import_structure = { "chat_template_utils": ["add_response_schema", "clone_chat_template", "get_training_chat_template"], "data_utils": [ "apply_chat_template", "extract_prompt", "is_conversational", "is_conversational_from_value", "maybe_apply_chat_template", "maybe_convert_to_chatml", "maybe_extract_prompt", "maybe_unpair_preference_dataset", "pack_dataset", "prepare_multimodal_messages", "prepare_multimodal_messages_vllm", "truncate_dataset", "unpair_preference_dataset", ], "models": ["create_reference_model"], "scripts": ["DatasetMixtureConfig", "ScriptArguments", "TrlParser", "get_dataset", "init_zero_verbose"], "trainer": [ "BEMACallback", "DPOConfig", "DPOTrainer", "GRPOConfig", "GRPOTrainer", "KTOConfig", "KTOTrainer", "LogCompletionsCallback", "ModelConfig", "RewardConfig", "RewardTrainer", "RichProgressCallback", "RLOOConfig", "RLOOTrainer", "SFTConfig", "SFTTrainer", "SyncRefModelCallback", "WeaveCallback", "get_kbit_device_map", "get_peft_config", "get_quantization_config", ], } if TYPE_CHECKING: from .chat_template_utils import add_response_schema, clone_chat_template, get_training_chat_template from .data_utils import ( apply_chat_template, extract_prompt, is_conversational, is_conversational_from_value, maybe_apply_chat_template, maybe_convert_to_chatml, maybe_extract_prompt, maybe_unpair_preference_dataset, pack_dataset, prepare_multimodal_messages, prepare_multimodal_messages_vllm, truncate_dataset, unpair_preference_dataset, ) from .models import create_reference_model from .scripts import DatasetMixtureConfig, ScriptArguments, TrlParser, get_dataset, init_zero_verbose from .trainer import ( BEMACallback, DPOConfig, DPOTrainer, GRPOConfig, GRPOTrainer, KTOConfig, KTOTrainer, LogCompletionsCallback, ModelConfig, RewardConfig, RewardTrainer, RichProgressCallback, RLOOConfig, RLOOTrainer, SFTConfig, SFTTrainer, SyncRefModelCallback, WeaveCallback, get_kbit_device_map, get_peft_config, get_quantization_config, ) else: import sys sys.modules[__name__] = _LazyModule( __name__, globals()["__file__"], _import_structure, module_spec=__spec__, extra_objects={"__version__": __version__}, ) ================================================ FILE: trl/_compat.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Compatibility shims for third-party dependencies. This module contains temporary patches to handle version incompatibilities between TRL's dependencies. Each patch should be removed when minimum version requirements eliminate the need. """ import warnings from packaging.version import Version from .import_utils import _is_package_available def _is_package_version_below(package_name: str, version_threshold: str) -> bool: """ Check if installed package version is below the given threshold. Args: package_name (str): Package name. version_threshold (str): Maximum version threshold. Returns: - True if package is installed and version < version_threshold. - False if package is not installed or version >= version_threshold. """ try: is_available, version = _is_package_available(package_name, return_version=True) return is_available and Version(version) < Version(version_threshold) except Exception as e: warnings.warn( f"Failed to check {package_name} version against {version_threshold}: {e}. " f"Compatibility patch may not be applied.", stacklevel=2, ) return False def _is_package_version_at_least(package_name: str, version_threshold: str) -> bool: """ Check if installed package version is at least the given threshold. Args: package_name (str): Package name. version_threshold (str): Minimum version threshold. Returns: - True if package is installed and version >= version_threshold. - False if package is not installed or version < version_threshold. """ try: is_available, version = _is_package_available(package_name, return_version=True) return is_available and Version(version) >= Version(version_threshold) except Exception as e: warnings.warn( f"Failed to check {package_name} version against {version_threshold}: {e}. " f"Compatibility patch may not be applied.", stacklevel=2, ) return False def _patch_vllm_logging() -> None: """Set vLLM logging level to ERROR by default to reduce noise.""" if _is_package_available("vllm"): import os os.environ["VLLM_LOGGING_LEVEL"] = os.getenv("VLLM_LOGGING_LEVEL", "ERROR") def _patch_vllm_disabled_tqdm() -> None: """ Fix DisabledTqdm class in vLLM. - Bug introduced in https://github.com/vllm-project/vllm/pull/52 - Fixed in https://github.com/vllm-project/vllm/pull/28471 (released in v0.11.1) - Since TRL currently supports vLLM v0.10.2-0.17.1, we patch it here - This can be removed when TRL requires vLLM>=0.11.1 """ if _is_package_version_below("vllm", "0.11.1"): try: import vllm.model_executor.model_loader.weight_utils from tqdm import tqdm class DisabledTqdm(tqdm): def __init__(self, *args, **kwargs): kwargs["disable"] = True super().__init__(*args, **kwargs) vllm.model_executor.model_loader.weight_utils.DisabledTqdm = DisabledTqdm except (ImportError, AttributeError) as e: warnings.warn(f"Failed to patch vLLM DisabledTqdm: {e}", stacklevel=2) def _patch_vllm_cached_tokenizer() -> None: """ Fix get_cached_tokenizer for transformers v5 compatibility. - Issue: vLLM's get_cached_tokenizer accesses all_special_tokens_extended - Removed in transformers: https://github.com/huggingface/transformers/pull/40936 (transformers>=5.0.0) - Fixed in https://github.com/vllm-project/vllm/pull/29686 (released in v0.12.0) - This can be removed when TRL requires vLLM>=0.12.0 """ if _is_package_version_at_least("transformers", "5.0.0") and _is_package_version_below("vllm", "0.12.0"): try: import contextlib import copy import vllm.transformers_utils.tokenizer def get_cached_tokenizer(tokenizer): cached_tokenizer = copy.copy(tokenizer) tokenizer_all_special_ids = tokenizer.all_special_ids tokenizer_all_special_tokens = tokenizer.all_special_tokens tokenizer_vocab = tokenizer.get_vocab() tokenizer_len = len(tokenizer) max_token_id = max(tokenizer_vocab.values()) if hasattr(tokenizer, "vocab_size"): with contextlib.suppress(NotImplementedError): max_token_id = max(max_token_id, tokenizer.vocab_size) class CachedTokenizer(tokenizer.__class__): # type: ignore @property def all_special_ids(self) -> list[int]: return tokenizer_all_special_ids @property def all_special_tokens(self) -> list[str]: return tokenizer_all_special_tokens @property def max_token_id(self) -> int: return max_token_id def get_vocab(self) -> dict[str, int]: return tokenizer_vocab def __len__(self) -> int: return tokenizer_len def __reduce__(self): return get_cached_tokenizer, (tokenizer,) CachedTokenizer.__name__ = f"Cached{tokenizer.__class__.__name__}" cached_tokenizer.__class__ = CachedTokenizer return cached_tokenizer vllm.transformers_utils.tokenizer.get_cached_tokenizer = get_cached_tokenizer except (ImportError, AttributeError) as e: warnings.warn(f"Failed to patch vLLM cached_tokenizer: {e}", stacklevel=2) def _patch_transformers_hybrid_cache() -> None: """ Fix HybridCache import for transformers v5 compatibility. - Issue: peft import HybridCache from transformers.cache_utils - HybridCache removed in https://github.com/huggingface/transformers/pull/43168 (transformers>=5.0.0) - Fixed in peft: https://github.com/huggingface/peft/pull/2735 (released in v0.18.0) - This can be removed when TRL requires peft>=0.18.0 """ if _is_package_version_at_least("transformers", "5.0.0") and _is_package_version_below("peft", "0.18.0"): try: import transformers.cache_utils from transformers.utils.import_utils import _LazyModule Cache = transformers.cache_utils.Cache # Patch for liger_kernel: Add HybridCache as an alias for Cache in the cache_utils module transformers.cache_utils.HybridCache = Cache # Patch for peft: Patch _LazyModule.__init__ to add HybridCache to transformers' lazy loading structures _original_lazy_module_init = _LazyModule.__init__ def _patched_lazy_module_init(self, name, *args, **kwargs): _original_lazy_module_init(self, name, *args, **kwargs) if name == "transformers": # Update _LazyModule's internal structures if hasattr(self, "_import_structure") and "cache_utils" in self._import_structure: if "HybridCache" not in self._import_structure["cache_utils"]: self._import_structure["cache_utils"].append("HybridCache") if hasattr(self, "_class_to_module"): self._class_to_module["HybridCache"] = "cache_utils" if hasattr(self, "__all__") and "HybridCache" not in self.__all__: self.__all__.append("HybridCache") self.HybridCache = Cache _LazyModule.__init__ = _patched_lazy_module_init except Exception as e: warnings.warn(f"Failed to patch transformers HybridCache compatibility: {e}", stacklevel=2) def _patch_transformers_parallelism_config() -> None: """ Fix ParallelismConfig for transformers compatibility. Ensure that ``transformers.training_args`` always defines the symbol `ParallelismConfig` so that Python's `typing.get_type_hints` can resolve annotations on `transformers.TrainingArguments` without raising a `NameError`. This is needed when running with ``accelerate<1.10.1``, where the module ``accelerate.parallelism_config`` did not exist and therefore the type alias is not imported by Transformers. See upstream fix PR in transformers#40818. - Issue: transformers imports ParallelismConfig only if accelerate>=1.10.1 and raises NameError if accelerate<1.10.1 - Fixed in transformers: https://github.com/huggingface/transformers/pull/40818 (released in v4.57.0) - This can be removed when TRL requires transformers>=4.57.0 or accelerate>=1.10.1 """ if _is_package_version_below("transformers", "4.57.0") and _is_package_version_below("accelerate", "1.10.1"): try: from typing import Any import transformers.training_args if not hasattr(transformers.training_args, "ParallelismConfig"): transformers.training_args.ParallelismConfig = Any except Exception as e: warnings.warn(f"Failed to patch transformers ParallelismConfig compatibility: {e}", stacklevel=2) # Apply vLLM patches _patch_vllm_logging() _patch_vllm_disabled_tqdm() _patch_vllm_cached_tokenizer() # Apply transformers patches _patch_transformers_hybrid_cache() _patch_transformers_parallelism_config() # before creating HfArgumentParser ================================================ FILE: trl/_lazy_module.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib import os from itertools import chain from types import ModuleType from typing import Any class _LazyModule(ModuleType): """ Module class that surfaces all objects but only performs associated imports when the objects are requested. """ # Very heavily inspired by optuna.integration._IntegrationModule # https://github.com/optuna/optuna/blob/master/optuna/integration/__init__.py def __init__(self, name, module_file, import_structure, module_spec=None, extra_objects=None): super().__init__(name) self._modules = set(import_structure.keys()) self._class_to_module = {} for key, values in import_structure.items(): for value in values: self._class_to_module[value] = key # Needed for autocompletion in an IDE self.__all__ = list(import_structure.keys()) + list(chain(*import_structure.values())) self.__file__ = module_file self.__spec__ = module_spec self.__path__ = [os.path.dirname(module_file)] self._objects = {} if extra_objects is None else extra_objects self._name = name self._import_structure = import_structure # Needed for autocompletion in an IDE def __dir__(self): result = super().__dir__() # The elements of self.__all__ that are submodules may or may not be in the dir already, depending on whether # they have been accessed or not. So we only add the elements of self.__all__ that are not already in the dir. for attr in self.__all__: if attr not in result: result.append(attr) return result def __getattr__(self, name: str) -> Any: if name in self._objects: return self._objects[name] if name in self._modules: value = self._get_module(name) elif name in self._class_to_module.keys(): module = self._get_module(self._class_to_module[name]) value = getattr(module, name) else: raise AttributeError(f"module {self.__name__} has no attribute {name}") setattr(self, name, value) return value def _get_module(self, module_name: str): try: return importlib.import_module("." + module_name, self.__name__) except Exception as e: raise RuntimeError( f"Failed to import {self.__name__}.{module_name} because of the following error (look up to see its" f" traceback):\n{e}" ) from e def __reduce__(self): return (self.__class__, (self._name, self.__file__, self._import_structure)) ================================================ FILE: trl/accelerate_configs/fsdp1.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: false fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_backward_prefetch: BACKWARD_PRE fsdp_cpu_ram_efficient_loading: true fsdp_forward_prefetch: true fsdp_offload_params: false fsdp_reshard_after_forward: FULL_SHARD fsdp_state_dict_type: FULL_STATE_DICT fsdp_sync_module_states: true fsdp_use_orig_params: true fsdp_version: 1 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/fsdp2.yaml ================================================ # Requires accelerate 1.7.0 or higher compute_environment: LOCAL_MACHINE debug: false distributed_type: FSDP downcast_bf16: 'no' enable_cpu_affinity: false fsdp_config: fsdp_activation_checkpointing: false fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_cpu_ram_efficient_loading: true fsdp_offload_params: false fsdp_reshard_after_forward: true fsdp_state_dict_type: FULL_STATE_DICT fsdp_version: 2 machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/multi_gpu.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: MULTI_GPU downcast_bf16: 'no' gpu_ids: all machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/single_gpu.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false distributed_type: "NO" downcast_bf16: 'no' gpu_ids: all machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/zero1.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard gradient_accumulation_steps: 1 zero3_init_flag: false zero_stage: 1 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/zero2.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard offload_optimizer_device: none offload_param_device: none zero3_init_flag: false zero_stage: 2 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: 'bf16' num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/accelerate_configs/zero3.yaml ================================================ compute_environment: LOCAL_MACHINE debug: false deepspeed_config: deepspeed_multinode_launcher: standard offload_optimizer_device: none offload_param_device: none zero3_init_flag: true zero3_save_16bit_model: true zero_stage: 3 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false ================================================ FILE: trl/chat_template_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from transformers import AddedToken, AutoTokenizer, PreTrainedModel, PreTrainedTokenizer def clone_chat_template( model: PreTrainedModel, tokenizer: PreTrainedTokenizer, source_tokenizer_path: str, resize_to_multiple_of: int | None = 64, ) -> tuple[PreTrainedModel, PreTrainedTokenizer, list[int]]: """ Clones a chat template from a source tokenizer to the target tokenizer and updates the model accordingly. This function: - Copies the chat template from a source tokenizer to the target tokenizer. - Adds any new tokens from the source tokenizer to the target tokenizer. - Sets and synchronizes the EOS token across the tokenizer and model. - Resizes the model's token embeddings to match the new vocabulary size, optionally rounding it up to a multiple of a specified value. In such cases, dummy tokens are added to the tokenizer to ensure the vocabulary size matches the embedding dimensions. Args: model ([`~transformers.PreTrainedModel`]): Model to update. tokenizer ([`~transformers.PreTrainedTokenizer`]): Tokenizer to update. source_tokenizer_path (`str`): Path or identifier of the pretrained tokenizer to clone from. resize_to_multiple_of (`int` or `None`, *optional*, defaults to `64`): The embedding layer will be resized to the new vocabulary size. If this is not `None`, it will round up the new vocabulary size to the nearest multiple of this value. Returns: model ([`~transformers.PreTrainedModel`]): Updated model with resized token embeddings and EOS token configured. tokenizer ([`~transformers.PreTrainedTokenizer`]): Updated tokenizer with the chat template and special tokens applied. added_tokens (`list[int]`): List of tokens that were added to the tokenizer from the source tokenizer. Example: ```python from transformers import AutoModelForCausalLM, AutoTokenizer from trl import clone_chat_template model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-1B") tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-1B") model, tokenizer, added_tokens = clone_chat_template(model, tokenizer, "Qwen/Qwen3-0.6B") ``` """ # Load the source tokenizer containing the desired chat template tokenizer_source = AutoTokenizer.from_pretrained(source_tokenizer_path) # Copy the chat template from the source tokenizer tokenizer.chat_template = tokenizer_source.get_chat_template() # Ensure all added tokens from the source are available in the target tokenizer added_tokens = [ token for token in tokenizer_source.added_tokens_decoder.values() if token.content not in tokenizer.vocab ] tokenizer.add_tokens(added_tokens) # Set the EOS token from the source tokenizer (important for generation) tokenizer.eos_token = tokenizer_source.eos_token model.config.eos_token_id = tokenizer.eos_token_id if model.can_generate(): # Non-generative models (e.g. SequenceClassification) may not have a generation_config model.generation_config.eos_token_id = tokenizer.eos_token_id # Resize model embeddings to include any new tokens, optionally rounding up to a multiple model.resize_token_embeddings( # After studying many tokenizers, we found that len(tokenizer.vocab) is the most reliable way to get the vocab # size. Avoid using tokenizer.vocab_size or tokenizer.vocab_size + len(tokenizer.added_tokens_encoder), # as handling of special and added tokens varies across tokenizers. new_num_tokens=len(tokenizer.vocab), pad_to_multiple_of=resize_to_multiple_of if resize_to_multiple_of is not None else None, ) # After resizing, the embedding matrix size may exceed the vocabulary size. Add dummy tokens to the tokenizer to # ensure vocabulary size matches the embedding matrix dimensions. idx = 0 while model.vocab_size > len(tokenizer.vocab): dummy_token = AddedToken(f"") is_added = tokenizer.add_tokens(dummy_token) idx += 1 if is_added == 1: added_tokens.append(dummy_token) # Verify that vocabulary size now matches embedding dimensions if len(tokenizer.vocab) != model.vocab_size: raise RuntimeError( f"Vocabulary size mismatch after resizing: tokenizer vocab size is {len(tokenizer.vocab)}, but model " f"embedding size is {model.vocab_size}. This indicates an internal error in the token alignment process." ) added_tokens = [token.content for token in added_tokens] added_tokens = tokenizer.convert_tokens_to_ids(added_tokens) return model, tokenizer, added_tokens # Adapted and corrected versions of the schemas from: # https://github.com/huggingface/transformers/blob/main/tests/utils/test_chat_parsing_utils.py qwen3_schema = { "x-regex": r"^(?:\n?(?:(?P.*?\S.*?)\n?|[\s]*)\s*)?(?P.*?)(?:\n(?=))?(?=(?:|<\|im_end\|>|$))(?P(?:.+?\s*)+)?\s*(?:<\|im_end\|>|$)", "type": "object", "properties": { "role": {"const": "assistant"}, "content": {"type": "string"}, "reasoning_content": {"type": "string"}, "tool_calls": { "type": "array", "x-regex-iterator": r"\s*(.+?)\s*", "items": { "x-parser": "json", "x-parser-args": {"transform": "{type: 'function', function: @}"}, "type": "object", "properties": { "type": {"const": "function"}, "function": { "type": "object", "properties": { "name": {"type": "string"}, "arguments": { "type": "object", "additionalProperties": {}, }, }, }, }, }, }, }, } qwen35_schema = { "x-regex": r"^(?:(?:\n?)?(?:(?P.*?\S.*?)\n?|[\s]*)\s*)?(?P.*?)(?:\n+(?=))?(?=(?:|<\|im_end\|>|$))(?P(?:.+?\s*)+)?\s*(?:<\|im_end\|>|$)", "type": "object", "properties": { "role": {"const": "assistant"}, "content": {"type": "string"}, "reasoning_content": {"type": "string"}, "tool_calls": { "type": "array", "x-regex-iterator": r"\s*(.+?)\s*", "items": { "type": "object", "properties": { "type": {"const": "function"}, "function": { "type": "object", "properties": { "name": {"type": "string", "x-regex": r"]+)>"}, "arguments": { "type": "object", "x-regex-key-value": r"[^>\n]+)>\n(?P.*?)\n", "default": {}, "additionalProperties": { "x-parser": "json", "x-parser-args": {"allow_non_json": True}, }, }, }, }, }, }, }, }, } # docstyle-ignore qwen3_chat_template = r"""{%- if tools %} {{- '<|im_start|>system\n' }} {%- if messages[0].role == 'system' %} {{- messages[0].content + '\n\n' }} {%- endif %} {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} {%- for tool in tools %} {{- "\n" }} {{- tool | tojson }} {%- endfor %} {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} {%- else %} {%- if messages[0].role == 'system' %} {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('') and message.content.endswith('')) %} {%- set ns.multi_step_tool = false %} {%- set ns.last_query_index = index %} {%- endif %} {%- endfor %} {%- for message in messages %} {%- if message.content is string %} {%- set content = message.content %} {%- else %} {%- set content = '' %} {%- endif %} {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }} {%- elif message.role == "assistant" %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is string %} {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {%- if loop.index0 > ns.last_query_index %} {%- if loop.last or (not loop.last and reasoning_content) %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- if message.tool_calls %} {%- for tool_call in message.tool_calls %} {%- if (loop.first and content) or (not loop.first) %} {{- '\n' }} {%- endif %} {%- if tool_call.function %} {%- set tool_call = tool_call.function %} {%- endif %} {{- '\n{"name": "' }} {{- tool_call.name }} {{- '", "arguments": ' }} {%- if tool_call.arguments is string %} {{- tool_call.arguments }} {%- else %} {{- tool_call.arguments | tojson }} {%- endif %} {{- '}\n' }} {%- endfor %} {%- endif %} {{- '<|im_end|>\n' }} {%- elif message.role == "tool" %} {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} {{- '<|im_start|>user' }} {%- endif %} {{- '\n\n' }} {{- content }} {{- '\n' }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} {{- '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- if enable_thinking is defined and enable_thinking is false %} {{- '\n\n\n\n' }} {%- endif %} {%- endif %}""" # docstyle-ignore qwen35_chat_template = r"""{%- set image_count = namespace(value=0) %} {%- set video_count = namespace(value=0) %} {%- macro render_content(content, do_vision_count, is_system_content=false) %} {%- if content is string %} {{- content }} {%- elif content is iterable and content is not mapping %} {%- for item in content %} {%- if 'image' in item or 'image_url' in item or item.type == 'image' %} {%- if is_system_content %} {{- raise_exception('System message cannot contain images.') }} {%- endif %} {%- if do_vision_count %} {%- set image_count.value = image_count.value + 1 %} {%- endif %} {%- if add_vision_id %} {{- 'Picture ' ~ image_count.value ~ ': ' }} {%- endif %} {{- '<|vision_start|><|image_pad|><|vision_end|>' }} {%- elif 'video' in item or item.type == 'video' %} {%- if is_system_content %} {{- raise_exception('System message cannot contain videos.') }} {%- endif %} {%- if do_vision_count %} {%- set video_count.value = video_count.value + 1 %} {%- endif %} {%- if add_vision_id %} {{- 'Video ' ~ video_count.value ~ ': ' }} {%- endif %} {{- '<|vision_start|><|video_pad|><|vision_end|>' }} {%- elif 'text' in item %} {{- item.text }} {%- else %} {{- raise_exception('Unexpected item type in content.') }} {%- endif %} {%- endfor %} {%- elif content is none or content is undefined %} {{- '' }} {%- else %} {{- raise_exception('Unexpected content type.') }} {%- endif %} {%- endmacro %} {%- if not messages %} {{- raise_exception('No messages provided.') }} {%- endif %} {%- if tools and tools is iterable and tools is not mapping %} {{- '<|im_start|>system\n' }} {{- "# Tools\n\nYou have access to the following functions:\n\n" }} {%- for tool in tools %} {{- "\n" }} {{- tool | tojson }} {%- endfor %} {{- "\n" }} {{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n\n\n\nvalue_1\n\n\nThis is the value for the second parameter\nthat can span\nmultiple lines\n\n\n\n\n\nReminder:\n- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n' }} {%- if messages[0].role == 'system' %} {%- set content = render_content(messages[0].content, false, true)|trim %} {%- if content %} {{- '\n\n' + content }} {%- endif %} {%- endif %} {{- '<|im_end|>\n' }} {%- else %} {%- if messages[0].role == 'system' %} {%- set content = render_content(messages[0].content, false, true)|trim %} {{- '<|im_start|>system\n' + content + '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if ns.multi_step_tool and message.role == "user" %} {%- set content = render_content(message.content, false)|trim %} {%- if not(content.startswith('') and content.endswith('')) %} {%- set ns.multi_step_tool = false %} {%- set ns.last_query_index = index %} {%- endif %} {%- endif %} {%- endfor %} {%- if ns.multi_step_tool %} {{- raise_exception('No user query found in messages.') }} {%- endif %} {%- for message in messages %} {%- set content = render_content(message.content, true)|trim %} {%- if message.role == "system" %} {%- if not loop.first %} {{- raise_exception('System message must be at the beginning.') }} {%- endif %} {%- elif message.role == "user" %} {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }} {%- elif message.role == "assistant" %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is string %} {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {%- set reasoning_content = reasoning_content|trim %} {%- if loop.index0 > ns.last_query_index %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content + '\n\n\n' + content }} {%- else %} {{- '<|im_start|>' + message.role + '\n' + content }} {%- endif %} {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %} {%- for tool_call in message.tool_calls %} {%- if tool_call.function is defined %} {%- set tool_call = tool_call.function %} {%- endif %} {%- if loop.first %} {%- if content|trim %} {{- '\n\n\n\n' }} {%- else %} {{- '\n\n' }} {%- endif %} {%- else %} {{- '\n\n\n' }} {%- endif %} {%- if tool_call.arguments is defined %} {%- for args_name, args_value in tool_call.arguments|items %} {{- '\n' }} {%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %} {{- args_value }} {{- '\n\n' }} {%- endfor %} {%- endif %} {{- '\n' }} {%- endfor %} {%- endif %} {{- '<|im_end|>\n' }} {%- elif message.role == "tool" %} {%- if loop.previtem and loop.previtem.role != "tool" %} {{- '<|im_start|>user' }} {%- endif %} {{- '\n\n' }} {{- content }} {{- '\n' }} {%- if not loop.last and loop.nextitem.role != "tool" %} {{- '<|im_end|>\n' }} {%- elif loop.last %} {{- '<|im_end|>\n' }} {%- endif %} {%- else %} {{- raise_exception('Unexpected message role.') }} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- if enable_thinking is defined and enable_thinking is true %} {{- '\n' }} {%- else %} {{- '\n\n\n\n' }} {%- endif %} {%- endif %}""" def add_response_schema(tokenizer: PreTrainedTokenizer) -> PreTrainedTokenizer: r""" Adds the appropriate response schema to the given tokenizer based on its chat template. At the time of initial implementation, most tokenizers do not have built-in support for response schemas. While waiting for broader adoption, we provide this utility function to manually set the response schema for known chat templates. Args: tokenizer (`PreTrainedTokenizer`): Tokenizer to which the response schema will be added. Returns: `PreTrainedTokenizer`: Tokenizer with the added response schema. Examples: ```python >>> from trl.chat_template_utils import add_response_schema >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") >>> tokenizer = add_response_schema(tokenizer) >>> assistant_text = '\n{"name": "multiply", "arguments": {"a": 3, "b": 4}}\n<|im_end|>' >>> tokenizer.parse_response(assistant_text) {'role': 'assistant', 'content': '', 'tool_calls': [{'type': 'function', 'function': {'name': 'multiply', 'arguments': {'a': 3, 'b': 4}}}]} ``` """ if tokenizer.chat_template == qwen3_chat_template: tokenizer.response_schema = qwen3_schema return tokenizer if tokenizer.chat_template == qwen35_chat_template: tokenizer.response_schema = qwen35_schema return tokenizer raise ValueError( "Unrecognized chat template, failed to add response schema. Please manually set the response schema on the " "tokenizer or processor. See the Transformers " "[docs](https://huggingface.co/docs/transformers/main/en/chat_response_parsing#response-parsing) for more " "details on response parsing." ) def is_chat_template_prefix_preserving(tokenizer: PreTrainedTokenizer) -> bool: """ Check whether the chat template preserves prefixes when applied. Args: tokenizer (`PreTrainedTokenizer`): Tokenizer instance to check. Returns: `bool`: `True` if the chat template preserves prefixes, `False` otherwise. """ messages1 = [ {"role": "user", "content": "What color is the sky?"}, ] messages2 = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, ] messages3 = [ {"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}, {"role": "user", "content": "And at night?"}, ] text1 = tokenizer.apply_chat_template(messages1, tokenize=False, add_generation_prompt=True) text2 = tokenizer.apply_chat_template(messages2, tokenize=False) text3 = tokenizer.apply_chat_template(messages3, tokenize=False) return text2.startswith(text1) and text3.startswith(text2) # Modifications: # - {%- if '' in content %} # + {%- if '' in content and '' in content %} # Always check for both tags to avoid edge cases where the model generates only one tag, which would otherwise be parsed incorrectly # - {%- if loop.index0 > ns.last_query_index %} ... {%- endif %} # + {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} # Always include thinking block during training. It's important to have a prefix-preserving template. # docstyle-ignore qwen3_training_chat_template = r"""{%- if tools %} {{- '<|im_start|>system\n' }} {%- if messages[0].role == 'system' %} {{- messages[0].content + '\n\n' }} {%- endif %} {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} {%- for tool in tools %} {{- "\n" }} {{- tool | tojson }} {%- endfor %} {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} {%- else %} {%- if messages[0].role == 'system' %} {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} {%- for message in messages[::-1] %} {%- set index = (messages|length - 1) - loop.index0 %} {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('') and message.content.endswith('')) %} {%- set ns.multi_step_tool = false %} {%- set ns.last_query_index = index %} {%- endif %} {%- endfor %} {%- for message in messages %} {%- if message.content is string %} {%- set content = message.content %} {%- else %} {%- set content = '' %} {%- endif %} {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }} {%- elif message.role == "assistant" %} {%- set reasoning_content = '' %} {%- if message.reasoning_content is string %} {%- set reasoning_content = message.reasoning_content %} {%- else %} {%- if '' in content and '' in content %} {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} {%- set content = content.split('')[-1].lstrip('\n') %} {%- endif %} {%- endif %} {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} {%- if message.tool_calls %} {%- for tool_call in message.tool_calls %} {%- if (loop.first and content) or (not loop.first) %} {{- '\n' }} {%- endif %} {%- if tool_call.function %} {%- set tool_call = tool_call.function %} {%- endif %} {{- '\n{"name": "' }} {{- tool_call.name }} {{- '", "arguments": ' }} {%- if tool_call.arguments is string %} {{- tool_call.arguments }} {%- else %} {{- tool_call.arguments | tojson }} {%- endif %} {{- '}\n' }} {%- endfor %} {%- endif %} {{- '<|im_end|>\n' }} {%- elif message.role == "tool" %} {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} {{- '<|im_start|>user' }} {%- endif %} {{- '\n\n' }} {{- content }} {{- '\n' }} {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} {{- '<|im_end|>\n' }} {%- endif %} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|im_start|>assistant\n' }} {%- if enable_thinking is defined and enable_thinking is false %} {{- '\n\n\n\n' }} {%- endif %} {%- endif %}""" # Modifications: # - {%- if '' in content %} # + {%- if '' in content and '' in content %} # Always check for both tags to avoid edge cases where the model generates only one tag, which would otherwise be parsed incorrectly # - {{- '<|im_start|>' + message.role + '\n' + content }} # + {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content + '\n\n\n' + content }} # Always include thinking block during training. It's important to have a prefix-preserving template. qwen35_training_chat_template = qwen35_chat_template.replace( "{%- if '' in content %}", "{%- if '' in content and '' in content %}", ).replace( "{{- '<|im_start|>' + message.role + '\\n' + content }}", "{{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content + '\\n\\n\\n' + content }}", ) def get_training_chat_template(tokenizer: PreTrainedTokenizer) -> str | None: r""" Get a prefix-preserving chat template for training, if needed. If the tokenizer's template isn't prefix-preserving, returns a training-compatible template (currently Qwen3 and Qwen3.5 supported). Otherwise, returns `None`. Args: tokenizer (`PreTrainedTokenizer`): Tokenizer instance to check. Returns: `str` or `None`: Training-compatible chat template, or `None` if no patching is needed. Example: ```python >>> from trl.chat_template_utils import get_training_chat_template >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") >>> messages1 = [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is blue."}, ... ] >>> messages2 = [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is blue."}, ... {"role": "user", "content": "And at night?"}, ... ] >>> tokenizer.apply_chat_template(messages1, tokenize=False) '<|im_start|>user\nWhat color is the sky?<|im_end|>\n<|im_start|>assistant\n\n\n\n\nIt is blue.<|im_end|>\n' >>> tokenizer.apply_chat_template(messages2, tokenize=False) '<|im_start|>user\nWhat color is the sky?<|im_end|>\n<|im_start|>assistant\nIt is blue.<|im_end|>\n<|im_start|>user\nAnd at night?<|im_end|>\n' >>> # ^ think tags missing >>> chat_template = get_training_chat_template(tokenizer) >>> tokenizer.apply_chat_template(messages1, tokenize=False, chat_template=chat_template) '<|im_start|>user\nWhat color is the sky?<|im_end|>\n<|im_start|>assistant\n\n\n\n\nIt is blue.<|im_end|>\n' >>> tokenizer.apply_chat_template(messages2, tokenize=False, chat_template=chat_template) '<|im_start|>user\nWhat color is the sky?<|im_end|>\n<|im_start|>assistant\n\n\n\n\nIt is blue.<|im_end|>\n<|im_start|>user\nAnd at night?<|im_end|>\n' ``` """ # First check if patching is needed if is_chat_template_prefix_preserving(tokenizer): return None # No patching needed if tokenizer.chat_template == qwen3_chat_template: return qwen3_training_chat_template if tokenizer.chat_template == qwen35_chat_template: return qwen35_training_chat_template else: raise ValueError( "The tokenizer's chat template is not prefix-preserving and patching is not supported for this template. " "Please manually modify the tokenizer's chat template for training." ) def _validate_tool_calls(tool_calls: list | None) -> None: """ Validate tool_calls to ensure all required fields exist with valid values. Raises ValueError when the model generates malformed tool calls (e.g., missing 'arguments' field) that are partially parsed. Args: tool_calls: List of tool call dictionaries, or None. """ if tool_calls is None: return None if not isinstance(tool_calls, list): raise ValueError("tool_calls must be a list or None.") for idx, tool_call in enumerate(tool_calls): if not isinstance(tool_call, dict): raise ValueError(f"tool_calls[{idx}] must be a dict.") # Handle nested function structure: {"type": "function", "function": {"name": ..., "arguments": ...}} if "function" in tool_call: func = tool_call["function"] if not isinstance(func, dict): raise ValueError(f"tool_calls[{idx}]['function'] must be a dict.") if not isinstance(func.get("name"), str): raise ValueError(f"tool_calls[{idx}]['function']['name'] must be a string.") # Some templates (e.g. Qwen3.5) omit arguments for valid no-arg calls; normalize to {}. if "arguments" not in func or func["arguments"] is None: func["arguments"] = {} else: # Handle flat structure: {"name": ..., "arguments": ...} if not isinstance(tool_call.get("name"), str): raise ValueError(f"tool_calls[{idx}]['name'] must be a string.") # Some templates (e.g. Qwen3.5) omit arguments for valid no-arg calls; normalize to {}. if "arguments" not in tool_call or tool_call["arguments"] is None: tool_call["arguments"] = {} def parse_response(tokenizer: PreTrainedTokenizer, ids: list[int]) -> dict: r""" Parse a token sequence into structured response dictionaries with fallback handling. Attempts to parse the sequence using `tokenizer.parse_response()`. If parsing fails (e.g., due to malformed tool calls like `{"type":"function"`), falls back to decoding as plain text. Also removes incorrectly appended EOS tokens from tool call content when present, and validates tool_calls to ensure all required fields exist. Args: tokenizer (`PreTrainedTokenizer`): Tokenizer with a `parse_response()` method. ids (`list[int]`): List of token sequences. Returns: `dict`: Response dictionary. Example: ```python >>> from trl.chat_template_utils import parse_response, add_response_schema >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B") >>> tokenizer = add_response_schema(tokenizer) # temporary until built-in support >>> text = '\n{"name": "multiply", "arguments": {"a": 3, "b": 4}}\n<|im_end|>' >>> ids = tokenizer(text)["input_ids"] >>> parse_response(tokenizer, ids) {'role': 'assistant', 'content': '', 'tool_calls': [{'type': 'function', 'function': {'name': 'multiply', 'arguments': {'a': 3, 'b': 4}}}]} ``` """ try: parsed = tokenizer.parse_response(ids) # Hotfix: remove incorrectly appended EOS token from tool calls # See https://github.com/huggingface/transformers/issues/42249 parsed["content"] = parsed["content"].removesuffix(tokenizer.eos_token) # Validate tool_calls to prevent Jinja2 Undefined errors when fields are missing if "tool_calls" in parsed: _validate_tool_calls(parsed["tool_calls"]) except (ValueError, TypeError): # Fallback: decode as plain text if parsing fails. This happens if the model outputs malformed tool calls. content = tokenizer.decode(ids, skip_special_tokens=True) parsed = {"role": "assistant", "content": content} return parsed ================================================ FILE: trl/cli/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .main import main __all__ = ["main"] ================================================ FILE: trl/cli/accelerate_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib.resources as resources from pathlib import Path def resolve_accelerate_config_argument(launch_args: list[str]) -> list[str]: """ Resolve `--accelerate_config` from CLI arguments into `accelerate --config_file`. The function supports either a filesystem path or a predefined config name shipped in `trl/accelerate_configs` (without the `.yaml` suffix). """ if "--accelerate_config" not in launch_args: return launch_args config_index = launch_args.index("--accelerate_config") if config_index + 1 >= len(launch_args): raise ValueError("Expected a value after `--accelerate_config`.") config_name = launch_args[config_index + 1] if Path(config_name).is_file(): accelerate_config_path = config_name else: candidate = resources.files("trl.accelerate_configs").joinpath(f"{config_name}.yaml") if not candidate.exists(): raise ValueError( f"Accelerate config {config_name} is neither a file nor a valid config in the `trl` package. " "Please provide a valid config name or a path to a config file." ) accelerate_config_path = candidate # Remove '--accelerate_config '. launch_args = launch_args[:config_index] + launch_args[config_index + 2 :] return ["--config_file", str(accelerate_config_path)] + launch_args ================================================ FILE: trl/cli/accelerate_launcher.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib.resources as resources from collections.abc import Callable from typing import Any from accelerate.commands.launch import launch_command, launch_command_parser def launch_training_script( script_name: str, launch_args: list[str], training_script_args: list[str], *, launch_command_fn: Callable[[Any], None] = launch_command, launch_parser_fn: Callable[[], Any] = launch_command_parser, ) -> None: """ Launch a TRL training script through `accelerate launch`. Parameters: script_name (`str`): Script filename in `trl/scripts`, e.g. `"dpo.py"`. launch_args (`list[str]`): Arguments consumed by `accelerate launch`. training_script_args (`list[str]`): Arguments forwarded to the training script. launch_command_fn (`Callable[[Any], None]`, *optional*): Function used to execute accelerate launch. launch_parser_fn (`Callable[[], Any]`, *optional*): Factory creating the accelerate launch parser. """ training_script = resources.files("trl.scripts").joinpath(script_name) accelerate_args = launch_parser_fn().parse_args(launch_args + [str(training_script)] + training_script_args) launch_command_fn(accelerate_args) ================================================ FILE: trl/cli/commands/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .base import Command from .env import EnvCommand from .skills import SkillsCommand from .training import TrainingCommand from .vllm_serve import VllmServeCommand def get_commands() -> list[Command]: """Return all registered top-level TRL CLI commands.""" return [ TrainingCommand("dpo"), EnvCommand(), TrainingCommand("grpo"), TrainingCommand("kto"), TrainingCommand("reward"), TrainingCommand("rloo"), TrainingCommand("sft"), SkillsCommand(), VllmServeCommand(), ] __all__ = ["Command", "get_commands"] ================================================ FILE: trl/cli/commands/base.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from abc import ABC, abstractmethod from argparse import Namespace from dataclasses import dataclass @dataclass(slots=True) class CommandContext: """Context shared by CLI commands during execution.""" argv: list[str] def argv_after(self, token: str) -> list[str]: """ Return CLI tokens after the first occurrence of `token`. Parameters: token (`str`): Subcommand name as it appears in `argv`. """ try: index = self.argv.index(token) except ValueError: return [] return self.argv[index + 1 :] class Command(ABC): """ Base command definition for the TRL CLI. Parameters: name (`str`): Subcommand name exposed by the CLI. help_text (`str`): Short description displayed in help output. """ def __init__(self, name: str, help_text: str): self.name = name self.help_text = help_text @abstractmethod def register(self, subparsers) -> None: """Register this command parser in the subparser collection.""" @abstractmethod def run(self, args: Namespace, context: CommandContext) -> int: """Execute the command.""" ================================================ FILE: trl/cli/commands/env.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from argparse import Namespace from .base import Command, CommandContext class EnvCommand(Command): """CLI command that prints TRL environment information.""" def __init__(self): super().__init__(name="env", help_text="Print the environment information") def register(self, subparsers) -> None: subparsers.add_parser(self.name, help=self.help_text) def run(self, args: Namespace, context: CommandContext) -> int: from ...scripts.env import print_env print_env() return 0 ================================================ FILE: trl/cli/commands/skills.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from argparse import Namespace from ...skills.cli import add_skills_subcommands from .base import Command, CommandContext class SkillsCommand(Command): """CLI command that manages TRL agent skills.""" def __init__(self): super().__init__(name="skills", help_text="Manage TRL agent skills") self._skills_parser = None def register(self, subparsers) -> None: self._skills_parser = subparsers.add_parser(self.name, help=self.help_text) skills_subparsers = self._skills_parser.add_subparsers(dest="skills_command", help="Skills commands") add_skills_subcommands(skills_subparsers) def run(self, args: Namespace, context: CommandContext) -> int: if getattr(args, "skills_command", None): if hasattr(args, "func"): return args.func(args) print("Error: Unknown skills command") return 1 if self._skills_parser is not None: self._skills_parser.print_help() return 0 ================================================ FILE: trl/cli/commands/training.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib from argparse import Namespace from .base import Command, CommandContext def _subtract_subsequence(lst: list[str], subseq: list[str]) -> list[str]: """Return lst with the ordered subsequence subseq removed.""" sub_iter = iter(subseq) current = next(sub_iter, None) result = [] for item in lst: if current is not None and item == current: current = next(sub_iter, None) else: result.append(item) return result class TrainingCommand(Command): """ Generic CLI command that launches a training script with accelerate. The script `trl/scripts/.py` must expose a `make_parser()` function. Parameters: name (`str`): CLI subcommand name (e.g. `"dpo"`). """ def __init__(self, name: str): super().__init__(name=name, help_text=f"Run the {name} training script") def register(self, subparsers) -> None: subparsers.add_parser(self.name, help=self.help_text, add_help=False) def run(self, args: Namespace, context: CommandContext) -> int: from ..accelerate_config import resolve_accelerate_config_argument from ..accelerate_launcher import launch_training_script module = importlib.import_module(f"...scripts.{self.name}", package=__package__) all_args = context.argv_after(self.name) parser = module.make_parser(prog=f"trl {self.name}") # Handles -h (exits). Returns config_remaining and cli_remaining separately. # cli_remaining is an ordered subsequence of all_args; config_remaining is not. *_, config_remaining, cli_remaining = parser.parse_args_and_config( all_args, return_remaining_strings=True, separate_remaining_strings=True ) launch_args = resolve_accelerate_config_argument(config_remaining + cli_remaining) training_script_args = _subtract_subsequence(all_args, cli_remaining) launch_training_script( script_name=f"{self.name}.py", launch_args=launch_args, training_script_args=training_script_args, ) return 0 ================================================ FILE: trl/cli/commands/vllm_serve.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from argparse import Namespace from .base import Command, CommandContext class VllmServeCommand(Command): """CLI command for serving TRL models with vLLM.""" def __init__(self): super().__init__(name="vllm-serve", help_text="Serve a model with vLLM") def register(self, subparsers) -> None: subparsers.add_parser(self.name, help=self.help_text, add_help=False) def run(self, args: Namespace, context: CommandContext) -> int: from ...scripts.vllm_serve import main as vllm_serve_main from ...scripts.vllm_serve import make_parser as make_vllm_serve_parser parser = make_vllm_serve_parser(prog="trl vllm-serve") (script_args,) = parser.parse_args_and_config(args=context.argv_after(self.name)) vllm_serve_main(script_args) return 0 ================================================ FILE: trl/cli/main.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from argparse import ArgumentParser from .commands import get_commands from .commands.base import Command, CommandContext def _build_parser(commands: list[Command]) -> ArgumentParser: parser = ArgumentParser(prog="trl", allow_abbrev=False) subparsers = parser.add_subparsers(help="available commands", dest="command") for command in commands: command.register(subparsers) return parser def main(argv: list[str] | None = None) -> int: """Run the TRL CLI.""" commands = get_commands() commands_by_name = {command.name: command for command in commands} parser = _build_parser(commands) argv = list(sys.argv[1:] if argv is None else argv) args, _ = parser.parse_known_args(argv) command_name = getattr(args, "command", None) if command_name is None: parser.print_help() return 0 command = commands_by_name[command_name] context = CommandContext(argv=argv) return command.run(args, context) if __name__ == "__main__": raise SystemExit(main()) ================================================ FILE: trl/data_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy from collections import defaultdict, deque from collections.abc import Callable, Sequence from itertools import takewhile from typing import Any, Literal, TypeVar import numpy as np import pyarrow as pa import pyarrow.compute as pc import pyarrow.types from datasets import Dataset, DatasetDict, IterableDatasetDict from transformers import PreTrainedTokenizerBase, ProcessorMixin DatasetType = TypeVar("DatasetType", Dataset, DatasetDict) def prepare_multimodal_messages(messages: list[dict[str, Any]], images: list) -> list[dict[str, Any]]: # docstyle-ignore # because is not parsable in the code block """ Convert messages into a structured multimodal format and inject the provided images into the message contents. Args: messages (`list[dict[str, Any]]`): Messages with `"role"`, `"content"` (or `"tool_calls"`). Content may be a raw string before transformation. List of messages with a `"role"` key (`"system"`, `"user"`, `"assistant"`, or `"tool"`) and a `"content"` key containing either a string or a list of structured blocks if already prepared. Optionally, the `"content"` might be `None` or not provided in favour of `"tool_calls"` in the `"assistant"` turns if applicable. images (`list`): List of image objects to insert. Can be empty if no images are included in the messages. Returns: `list[dict[str, Any]]`: A deep-copied list of messages where every `"content"` value is a list of structured content blocks, and all `"image"` placeholders are populated with the corresponding image objects. If the assistant turns contains `"tool_calls"`, then the `"content"` might be empty. Notes: - When the input `messages` isn't already in the structured format, (i.e., all `"content"` values are strings), the function transforms them into the structured format by wrapping text in `{"type": "text", "text": ...}` and inserting `{"type": "image"}` placeholders for the images *before* the first user message. If the number of placeholders does not match the number of provided images, an error is raised. - When the input `messages` contains either `"tool_calls"` in the `"assistant"` turns, or `"tool"` roles with `"content"` and `"name"` those are left as-is, since those don't require any specific handling for multimodal data. Example: ```python # Input [ {"role": "user", "content": "What's in this image?"}, {"role": "assistant", "content": "It looks like a cat."}, ] # Output, one image provided [ {"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What's in this image?"}]}, {"role": "assistant", "content": [{"type": "text", "text": "It looks like a cat."}]}, ] ``` """ messages = copy.deepcopy(messages) # avoid modifying the original messages # First, convert all messages to the structured format if needed, and insert image placeholders if needed images_included = False for message in messages: if message["role"] == "system": if isinstance(message["content"], str): # if already prepared, the content will be a list message["content"] = [{"type": "text", "text": message["content"]}] elif message["role"] == "user": if isinstance(message["content"], str) and not images_included: image_entries = [{"type": "image"} for _ in range(len(images))] message["content"] = [*image_entries, {"type": "text", "text": message["content"]}] images_included = True elif isinstance(message["content"], str) and images_included: message["content"] = [{"type": "text", "text": message["content"]}] elif message["role"] == "assistant": if message.get("content") and isinstance(message["content"], str): message["content"] = [{"type": "text", "text": message["content"]}] elif message["role"] == "tool": # NOTE: `tool` contains `name` (name of the tool used) and `content` (output of the tool call as a string) # but there's no need to prepare it for multimodal specifically but rather leave it as-is continue else: raise ValueError( f"Invalid role in message: {message['role']}. Expected 'system', 'user', 'assistant', or 'tool'." ) # Then, check that the number of image placeholders matches the number of images provided num_placeholders = sum( sum(1 for part in message["content"] if part["type"] == "image") for message in messages if message.get("content") and message["role"] != "tool" ) if num_placeholders != len(images): raise ValueError( f"Number of images provided ({len(images)}) does not match number of image placeholders ({num_placeholders})." ) # Then, fill in the actual images in the placeholders img_idx = 0 for message in messages: if not message.get("content") or message["role"] == "tool": continue for part in message["content"]: if part["type"] == "image": part["image"] = images[img_idx] img_idx += 1 return messages def prepare_multimodal_messages_vllm(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: # docstyle-ignore # because is not parsable in the code block """ Convert structured multimodal messages into a format compatible with vLLM. Replaces `"type": "image"` blocks with `"type": "image_pil"` blocks, and `"image": Image` with `"image_pil": Image`. Args: messages (`list[dict[str, Any]]`): Messages with `"role"` and `"content"`. Content is expected to be a list of structured blocks. Returns: `list[dict[str, Any]]`: A deep-copied list of messages compatible with vLLM's expected input format. Example: ```python # Input [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What's in this image?"}]}] # Output [{"role": "user", "content": [{"type": "image_pil", "image_pil": }, {"type": "text", "text": "What's in this image?"}]}] ``` """ messages = copy.deepcopy(messages) # avoid modifying the original messages for message in messages: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image": part["type"] = "image_pil" # vLLM expects 'image_pil' key for images part["image_pil"] = part.pop("image") return messages def is_conversational(example: dict[str, Any]) -> bool: r""" Check if the example is in a conversational format. Args: example (`dict[str, Any]`): A single data entry of a dataset. The example can have different keys depending on the dataset type. Returns: `bool`: `True` if the data is in a conversational format, `False` otherwise. Examples: ```python >>> example = {"prompt": [{"role": "user", "content": "What color is the sky?"}]} >>> is_conversational(example) True >>> example = {"prompt": "The sky is"} >>> is_conversational(example) False ``` """ supported_keys = ["prompt", "chosen", "rejected", "completion", "messages"] example_keys = {key for key in example.keys() if key in supported_keys} # It must have one of the supported keys if example_keys: key = example_keys.pop() # take the first supported key maybe_messages = example[key] # It must be a list of messages if isinstance(maybe_messages, list): maybe_message = maybe_messages[0] # Each message must a list of dictionaries with keys "role" and "content" if isinstance(maybe_message, dict) and "role" in maybe_message: return True return False def apply_chat_template( example: dict[str, list[dict[str, str]]], tokenizer: PreTrainedTokenizerBase | ProcessorMixin, tools: list[dict | Callable] | None = None, **template_kwargs, ) -> dict[str, str]: r""" Apply a chat template to a conversational example along with the schema for a list of functions in `tools`. For more details, see [`maybe_apply_chat_template`]. """ # Check that the example has the correct keys supported_keys = ["prompt", "chosen", "rejected", "completion", "messages", "label"] example_keys = {key for key in example.keys() if key in supported_keys} if example_keys not in [ {"messages"}, # language modeling {"prompt"}, # prompt-only {"prompt", "completion"}, # prompt-completion {"prompt", "chosen", "rejected"}, # preference {"chosen", "rejected"}, # preference with implicit prompt {"prompt", "completion", "label"}, # unpaired preference ]: raise KeyError(f"Invalid keys in the example: {example_keys}") # Apply the chat template to the whole conversation if "messages" in example: messages = tokenizer.apply_chat_template( example["messages"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # Apply the chat template to the prompt, adding the generation prompt if "prompt" in example: last_role = example["prompt"][-1]["role"] if last_role in ["user", "tool"]: add_generation_prompt = True continue_final_message = False elif last_role == "assistant": add_generation_prompt = False continue_final_message = True else: raise ValueError(f"Invalid role in the last message: {last_role}") prompt = tokenizer.apply_chat_template( example["prompt"], tools=tools, continue_final_message=continue_final_message, tokenize=False, add_generation_prompt=add_generation_prompt, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # Apply the chat template to the entire prompt + completion if "prompt" in example: # explicit prompt and prompt-completion case if "chosen" in example: prompt_chosen = tokenizer.apply_chat_template( example["prompt"] + example["chosen"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # DeepSeek-R1 inserts a token when using `add_generation_prompt`, which can cause discrepancies # between the prompt alone and the combined prompt+completion. To ensure consistency, we extract the # common prefix between the two. In most cases, this is a no-op. prompt = "".join(x for x, _ in takewhile(lambda x: x[0] == x[1], zip(prompt, prompt_chosen, strict=False))) chosen = prompt_chosen[len(prompt) :] if "rejected" in example and "prompt" in example: # explicit prompt prompt_rejected = tokenizer.apply_chat_template( example["prompt"] + example["rejected"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # Handle DeepSeek-R1 token, see the above comment for details prompt = "".join( x for x, _ in takewhile(lambda x: x[0] == x[1], zip(prompt, prompt_rejected, strict=False)) ) rejected = prompt_rejected[len(prompt) :] if "completion" in example: prompt_completion = tokenizer.apply_chat_template( example["prompt"] + example["completion"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # Handle DeepSeek-R1 token, see the above comment for details prompt = "".join( x for x, _ in takewhile(lambda x: x[0] == x[1], zip(prompt, prompt_completion, strict=False)) ) completion = prompt_completion[len(prompt) :] else: # implicit prompt case if "chosen" in example: chosen = tokenizer.apply_chat_template( example["chosen"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) if "rejected" in example: rejected = tokenizer.apply_chat_template( example["rejected"], tools=tools, tokenize=False, **example.get("chat_template_kwargs", {}), **template_kwargs, ) # Extract the completion by removing the prompt part from the prompt-completion string output = {} if "messages" in example: output["text"] = messages if "prompt" in example: output["prompt"] = prompt if "chosen" in example: output["chosen"] = chosen if "rejected" in example: output["rejected"] = rejected if "completion" in example: output["completion"] = completion if "label" in example: output["label"] = example["label"] return output def maybe_apply_chat_template( example: dict[str, list[dict[str, str]]], tokenizer: PreTrainedTokenizerBase, tools: list[dict | Callable] | None = None, **template_kwargs: Any, ) -> dict[str, str]: r""" If the example is in a conversational format, apply a chat template to it. Args: example (`dict[str, list[dict[str, str]]`): Dictionary representing a single data entry of a conversational dataset. Each data entry can have different keys depending on the dataset type. The supported dataset types are: - Language modeling dataset: `"messages"`. - Prompt-only dataset: `"prompt"`. - Prompt-completion dataset: `"prompt"` and `"completion"`. - Preference dataset: `"prompt"`, `"chosen"`, and `"rejected"`. - Preference dataset with implicit prompt: `"chosen"` and `"rejected"`. - Unpaired preference dataset: `"prompt"`, `"completion"`, and `"label"`. For keys `"messages"`, `"prompt"`, `"chosen"`, `"rejected"`, and `"completion"`, the values are lists of messages, where each message is a dictionary with keys `"role"` and `"content"`. Additionally, the example may contain a `"chat_template_kwargs"` key, which is a dictionary of additional keyword arguments to pass to the chat template renderer. tokenizer ([`~transformers.PreTrainedTokenizerBase`]): Tokenizer to apply the chat template with. tools (`list[dict | Callable]`, *optional*): A list of tools (callable functions) that will be accessible to the model. If the template does not support function calling, this argument will have no effect. **template_kwargs (`Any`, *optional*): Additional kwargs to pass to the template renderer. Will be accessible by the chat template. Returns: `dict[str, str]`: Formatted example with the chat template applied. Notes: - This function does not alter the keys, except for Language modeling dataset, where `"messages"` is replaced by `"text"`. - In case of prompt-only data, if the last role is `"user"`, the generation prompt is added to the prompt. Else, if the last role is `"assistant"`, the final message is continued. Example: ```python >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-128k-instruct") >>> example = { ... "prompt": [{"role": "user", "content": "What color is the sky?"}], ... "completion": [{"role": "assistant", "content": "It is blue."}], ... } >>> apply_chat_template(example, tokenizer) {'prompt': '<|user|>\nWhat color is the sky?<|end|>\n<|assistant|>\n', 'completion': 'It is blue.<|end|>\n'} ``` """ if is_conversational(example): return apply_chat_template(example, tokenizer, tools, **template_kwargs) else: return example def _unpair_row(examples: list[dict[str, list[dict[str, str]]]]) -> list[dict[str, list[dict[str, str]]]]: batch_size = len(examples["chosen"]) new_rows = { "completion": examples["chosen"] + examples["rejected"], "label": [True] * batch_size + [False] * batch_size, } if "prompt" in examples: new_rows["prompt"] = examples["prompt"] + examples["prompt"] return new_rows def unpair_preference_dataset( dataset: DatasetType, num_proc: int | None = None, desc: str | None = None ) -> DatasetType: r""" Unpair a preference dataset. Args: dataset ([`~datasets.Dataset`] or [`~datasets.DatasetDict`]): Preference dataset to unpair. The dataset must have columns `"chosen"`, `"rejected"` and optionally `"prompt"`. num_proc (`int`, *optional*): Number of processes to use for processing the dataset. desc (`str`, *optional*): Meaningful description to be displayed alongside with the progress bar while mapping examples. Returns: [`~datasets.Dataset`]: The unpaired preference dataset. Example: ```python >>> from datasets import Dataset >>> dataset_dict = { ... "prompt": ["The sky is", "The sun is"], ... "chosen": [" blue.", "in the sky."], ... "rejected": [" green.", " in the sea."], ... } >>> dataset = Dataset.from_dict(dataset_dict) >>> dataset = unpair_preference_dataset(dataset) >>> dataset Dataset({ features: ['prompt', 'completion', 'label'], num_rows: 4 }) >>> dataset[0] {'prompt': 'The sky is', 'completion': ' blue.', 'label': True} ``` """ return dataset.map(_unpair_row, batched=True, remove_columns=["chosen", "rejected"], num_proc=num_proc, desc=desc) def maybe_unpair_preference_dataset( dataset: DatasetType, num_proc: int | None = None, desc: str | None = None ) -> DatasetType: r""" Unpair a preference dataset if it is paired. Args: dataset ([`~datasets.Dataset`] or [`~datasets.DatasetDict`]): Preference dataset to unpair. The dataset must have columns `"chosen"`, `"rejected"` and optionally `"prompt"`. num_proc (`int`, *optional*): Number of processes to use for processing the dataset. desc (`str`, *optional*): Meaningful description to be displayed alongside with the progress bar while mapping examples. Returns: [`~datasets.Dataset`] or [`~datasets.DatasetDict`]: The unpaired preference dataset if it was paired, otherwise the original dataset. Example: ```python >>> from datasets import Dataset >>> dataset_dict = { ... "prompt": ["The sky is", "The sun is"], ... "chosen": [" blue.", "in the sky."], ... "rejected": [" green.", " in the sea."], ... } >>> dataset = Dataset.from_dict(dataset_dict) >>> dataset = unpair_preference_dataset(dataset) >>> dataset Dataset({ features: ['prompt', 'completion', 'label'], num_rows: 4 }) >>> dataset[0] {'prompt': 'The sky is', 'completion': ' blue.', 'label': True} ``` """ if isinstance(dataset, DatasetDict): column_names = dataset[list(dataset.keys())[0]].column_names else: column_names = dataset.column_names if "chosen" in column_names and "rejected" in column_names: return unpair_preference_dataset(dataset, num_proc=num_proc, desc=desc) else: return dataset def extract_prompt(example: dict[str, Sequence]) -> dict[str, Sequence]: r""" Extracts the shared prompt from a preference data example, where the prompt is implicit within both the chosen and rejected completions. The function identifies the longest common sequence (prefix) of conversation turns between the "chosen" and "rejected" completions and extracts this as the prompt. It then removes this prompt from the respective "chosen" and "rejected" completions. Args: example (`dict[str, list]`): A dictionary representing a single data entry in the preference dataset. It must contain the keys `"chosen"` and `"rejected"`, where each value is either conversational or standard (`str`). Returns: `dict[str, list]`: A dictionary containing: - `"prompt"`: The longest common prefix between the "chosen" and "rejected" completions. - `"chosen"`: The remainder of the "chosen" completion, with the prompt removed. - `"rejected"`: The remainder of the "rejected" completion, with the prompt removed. Examples: ```python >>> example = { ... "chosen": [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is blue."}, ... ], ... "rejected": [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is green."}, ... ], ... } >>> extract_prompt(example) {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'chosen': [{'role': 'assistant', 'content': 'It is blue.'}], 'rejected': [{'role': 'assistant', 'content': 'It is green.'}]} ``` Or, with the `map` method of [`~datasets.Dataset`]: ```python >>> from trl import extract_prompt >>> from datasets import Dataset >>> dataset_dict = { ... "chosen": [ ... [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is blue."}, ... ], ... [ ... {"role": "user", "content": "Where is the sun?"}, ... {"role": "assistant", "content": "In the sky."}, ... ], ... ], ... "rejected": [ ... [ ... {"role": "user", "content": "What color is the sky?"}, ... {"role": "assistant", "content": "It is green."}, ... ], ... [ ... {"role": "user", "content": "Where is the sun?"}, ... {"role": "assistant", "content": "In the sea."}, ... ], ... ], ... } >>> dataset = Dataset.from_dict(dataset_dict) >>> dataset = dataset.map(extract_prompt) >>> dataset[0] {'prompt': [{'role': 'user', 'content': 'What color is the sky?'}], 'chosen': [{'role': 'assistant', 'content': 'It is blue.'}], 'rejected': [{'role': 'assistant', 'content': 'It is green.'}]} ``` """ for idx in range(min(len(example["chosen"]), len(example["rejected"]))): if example["chosen"][idx] != example["rejected"][idx]: if example["chosen"][idx - 1] == " ": # remove space before the prompt idx -= 1 break return { "prompt": example["chosen"][:idx], "chosen": example["chosen"][idx:], "rejected": example["rejected"][idx:], } def maybe_extract_prompt(example: dict[str, list]) -> dict[str, list]: r""" Extracts the shared prompt from a preference data example, where the prompt is implicit within both the chosen and rejected completions. If the example already contains a `"prompt"` key, the function returns the example as is. For more details, see [`extract_prompt`]. ``` """ # Some dataset add a `"prompt"` column, even though the prompt is implicit and included in the "chosen" and # "rejected" completions. E.g.: # {"prompt": "What color is the sky?", # "chosen": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is blue."}], # "rejected": [{"role": "user", "content": "What color is the sky?"}, {"role": "assistant", "content": "It is green."}]} # That's why we check if the prompt is also conversational before deciding not to extract it. if "chosen" not in example or "rejected" not in example: # not a preference example return example if "prompt" in example: # Both conversational or both non-conversational chosen_conv = is_conversational({"chosen": example["chosen"]}) prompt_conv = is_conversational({"prompt": example["prompt"]}) if (chosen_conv and prompt_conv) or (not chosen_conv and not prompt_conv): return example return extract_prompt({"chosen": example["chosen"], "rejected": example["rejected"]}) def _get_dataset_format(dataset: DatasetType) -> dict[str, Any]: if isinstance(dataset, (DatasetDict, IterableDatasetDict)): dataset = dataset[next(iter(dataset))] if isinstance(dataset, Dataset): format = dataset.format else: format_type = dataset._formatting.format_type if dataset._formatting is not None else None format = {"type": format_type} format.update(format.pop("format_kwargs", {})) return format def _check_if_columns_can_be_packed(columns: list[pa.Array]): first_column_offsets = None for idx, column in enumerate(columns): if not (pyarrow.types.is_list(column.type) or pyarrow.types.is_large_list(column.type)): raise TypeError("Packing requires all columns to be lists of lists.") if idx == 0: first_column_offsets = column.offsets elif not first_column_offsets.equals(column.offsets): raise ValueError("All columns must have values of the same length.") class _SegmentTree: """ A segment tree data structure that, when initialized as `_SegmentTree(maxval)`, efficiently finds the next larger value for a given input within the range [1, maxval]. See [Fewer Truncations Improve Language Modeling](https://huggingface.co/papers/2404.10830) for more details. """ def __init__(self, maxval: int): self.maxval = maxval # For non-power-of-2 values, we need to round up to the next power of 2 for the tree size self.tree_size = 1 << (maxval - 1).bit_length() self.tree = [0] * (2 * self.tree_size) def add(self, val): assert 0 < val <= self.maxval i = self.tree_size + val - 1 self.tree[i] = val while i > 1: i >>= 1 left, right = self.tree[i << 1], self.tree[(i << 1) + 1] # Compare the values using if-else otherwise repeated calls to `builtins.max` become the bottleneck self.tree[i] = left if left >= right else right def remove(self, val): assert 0 < val <= self.maxval i = self.tree_size + val - 1 self.tree[i] = 0 while i > 1: i >>= 1 left, right = self.tree[i << 1], self.tree[(i << 1) + 1] # Compare the values using if-else otherwise repeated calls to `builtins.max` become the bottleneck self.tree[i] = left if left >= right else right def search(self, val): assert 0 < val <= self.maxval i = 1 while i < self.tree_size: if self.tree[i << 1] >= val: i = i << 1 else: i = (i << 1) + 1 return self.tree[i] def _pack_bfd( examples: pa.Table, seq_length: int, on_seq_length_overflow: Literal["truncate", "split"] = "truncate" ) -> pa.Table: """Pack sequences in a pyarrow Table using Best Fit Decreasing strategy.""" columns = [column.chunks[0] for column in examples.combine_chunks().columns] _check_if_columns_can_be_packed(columns) assert len(columns) > 0 lengths = pc.list_value_length(columns[0]) # Filter out empty sequences non_empty_mask = pc.greater(lengths, 0) columns = [pc.filter(column, non_empty_mask) for column in columns] lengths = pc.filter(lengths, non_empty_mask) if on_seq_length_overflow == "truncate": columns = [pc.list_slice(column, 0, seq_length) for column in columns] elif on_seq_length_overflow == "split": lengths = lengths.to_numpy() # Split the sequences longer than `seq_length` into chunks (of length `seq_length` or less) while respecting sequence boundaries num_fragments = np.ceil(lengths / seq_length).astype(int) offsets = np.arange(np.sum(num_fragments) + 1, dtype=columns[0].offsets.type.to_pandas_dtype()) * seq_length # "Left-shift" the offsets to account for the last fragment of each original sequence possibly being shorter than `seq_length` diff = np.zeros_like(offsets) diff[np.cumsum(num_fragments)] = -lengths % seq_length diff = np.cumsum(diff) offsets -= diff columns = [ type(column).from_arrays(offsets.astype(column.offsets.type.to_pandas_dtype()), column.values) for column in columns ] else: raise ValueError(f"Invalid `on_seq_length_overflow`: {on_seq_length_overflow}. Use 'truncate' or 'split'.") examples = pa.Table.from_arrays(columns, names=examples.column_names) lengths = pc.list_value_length(columns[0]) examples = examples.append_column("seq_lengths", lengths) # Allows us to later construct `position_ids` ids = np.arange(len(examples)) lengths = pc.make_struct(lengths, ids) lengths = lengths.sort("descending", by=0) # Greedy BFD binning using a segment tree to quickly find best-fit remaining space. segment_tree = _SegmentTree(seq_length) segment_tree.add(seq_length) # the max, `seq_length` bin is always available space_to_bin = defaultdict(deque) # Bin is represented as a dict (of example ids and sum of their lengths) to allow in-place updates bins: list[dict] = [] for length, idx in zip(lengths.field(0).to_numpy(), lengths.field(1).to_numpy(), strict=True): space = segment_tree.search(length) if space < seq_length: # Use existing bin with exactly this amount of space bin = space_to_bin[space].popleft() else: # Create a new bin bin = {"ids": [], "length": 0} bins.append(bin) bin["ids"].append(idx) bin["length"] += length if space < seq_length and not space_to_bin[space]: segment_tree.remove(space) space = space - length space_to_bin[space].append(bin) if space > 0: segment_tree.add(space) examples = pc.take(examples, [id_ for bin in bins for id_ in bin["ids"]]) offsets = np.cumsum([0] + [bin["length"] for bin in bins]) assert all( column.num_chunks == 1 for column in examples.columns ) # `pc.take` returns a ChunkedArray with a single chunk lengths = examples["seq_lengths"].chunks[0] examples = examples.drop_columns("seq_lengths") lengths = pa.ListArray.from_arrays(np.cumsum([0] + [len(bin["ids"]) for bin in bins], dtype=np.int32), lengths) columns = [] for column in examples.columns: column = column.chunks[0] assert pa.types.is_list(column.type) or pa.types.is_large_list(column.type) dtype = column.offsets.type.to_pandas_dtype() column = type(column).from_arrays(offsets.astype(dtype), column.values) columns.append(column) return pa.Table.from_arrays(columns + [lengths], names=examples.column_names + ["seq_lengths"]) def _pack_wrapped(examples: pa.Table, seq_length: int) -> pa.Table: """Pack sequences in a pyarrow Table using a wrapped strategy.""" columns = [column.chunks[0] for column in examples.combine_chunks().columns] _check_if_columns_can_be_packed(columns) offsets, values = columns[0].offsets, columns[0].values values = values[offsets[0].as_py() : offsets[-1].as_py()] num_elements = len(values) offsets = np.arange(0, num_elements, seq_length, dtype=columns[0].offsets.type.to_pandas_dtype()) offsets = np.concatenate((offsets, [num_elements])) columns = [ type(column).from_arrays(offsets.astype(column.offsets.type.to_pandas_dtype()), column.values) for column in columns ] return pa.Table.from_arrays(columns, names=examples.column_names) def pack_dataset( dataset: DatasetType, seq_length: int, strategy: str = "bfd", map_kwargs: dict[str, Any] | None = None, ) -> DatasetType: r""" Pack sequences in a dataset into chunks of size `seq_length`. Args: dataset ([`~datasets.Dataset`] or [`~datasets.DatasetDict`]): Dataset to pack seq_length (`int`): Target sequence length to pack to. strategy (`str`, *optional*, defaults to `"bfd"`): Packing strategy to use. Can be either: - `"bfd"` (Best Fit Decreasing): Preserves sequence boundaries and truncates sequences that exceed `seq_length`, discarding overflow tokens. Ideal for SFT and conversational datasets where maintaining conversation structure is important. - `"bfd_split"`: Similar to `"bfd"` but splits overflow sequences for packing into other examples. Prevents token loss for pre-training or long documents, but may break conversation structure in SFT datasets. - `"wrapped"`: Faster but more aggressive. Ignores sequence boundaries and will cut sequences in the middle to completely fill each packed sequence with data. map_kwargs (`dict`, *optional*): Additional keyword arguments to pass to the dataset's map method when packing examples. Returns: [`~datasets.Dataset`] or [`~datasets.DatasetDict`]: The dataset with packed sequences. The number of examples may decrease as sequences are combined. Example: ```python >>> from datasets import Dataset >>> from trl import pack_dataset >>> examples = { ... "input_ids": [[1, 2, 3, 4, 5], [6, 7], [8, 9, 10], [11]], ... "attention_mask": [[1, 1, 1, 0, 0], [1, 0], [1, 1, 0], [1]], ... } >>> dataset = Dataset.from_dict(examples) >>> # Default "bfd" strategy (SFT-friendly): truncates long sequences >>> packed_dataset = pack_dataset(dataset, seq_length=4, strategy="bfd") >>> packed_dataset[:] {'input_ids': [[1, 2, 3, 4], [8, 9, 10, 11], [6, 7]], 'attention_mask': [[1, 1, 1, 0], [1, 1, 0, 1], [1, 0]], 'seq_lengths': [[4], [3, 1], [2]]} >>> # "bfd_split" strategy: preserves all tokens >>> packed_dataset = pack_dataset(dataset, seq_length=4, strategy="bfd_split") >>> packed_dataset[:] {'input_ids': [[1, 2, 3, 4], [8, 9, 10, 5], [6, 7, 11]], 'attention_mask': [[1, 1, 1, 0], [1, 1, 0, 0], [1, 0, 1]], 'seq_lengths': [[4], [3, 1], [2, 1]]} ``` """ if map_kwargs is None: map_kwargs = {} valid_strategies = ("bfd", "bfd_split", "wrapped") if strategy not in valid_strategies: raise ValueError(f"Invalid packing strategy '{strategy}', must be one of {valid_strategies}.") format = _get_dataset_format(dataset) dataset = dataset.with_format("arrow") if strategy == "bfd": dataset = dataset.map( _pack_bfd, batched=True, fn_kwargs={"seq_length": seq_length, "on_seq_length_overflow": "truncate"}, **map_kwargs, ) elif strategy == "bfd_split": dataset = dataset.map( _pack_bfd, batched=True, fn_kwargs={"seq_length": seq_length, "on_seq_length_overflow": "split"}, **map_kwargs, ) elif strategy == "wrapped": dataset = dataset.map(_pack_wrapped, batched=True, fn_kwargs={"seq_length": seq_length}, **map_kwargs) else: raise ValueError(f"Invalid packing strategy: '{strategy}', must be one of {valid_strategies}.") if strategy in {"bfd", "bfd_split"} and "columns" in format: format["columns"] = format["columns"] + ["seq_lengths"] dataset = dataset.with_format(**format) return dataset def truncate_dataset( dataset: DatasetType, max_length: int, truncation_mode: str = "keep_start", map_kwargs: dict[str, Any] | None = None, ) -> DatasetType: r""" Truncate sequences in a dataset to a specified `max_length`. Args: dataset ([`~datasets.Dataset`] or [`~datasets.DatasetDict`]): Dataset to truncate. max_length (`int`): Maximum sequence length to truncate to. truncation_mode (`str`, *optional*, defaults to `"keep_start"`): Whether to keep the start (`"keep_start"`) or the end (`"keep_end"`) of the sequence when truncating. map_kwargs (`dict`, *optional*): Additional keyword arguments to pass to the dataset's map method when truncating examples. Returns: [`~datasets.Dataset`] or [`~datasets.DatasetDict`]: The dataset with truncated sequences. Example: ```python >>> from datasets import Dataset >>> examples = { ... "input_ids": [[1, 2, 3], [4, 5, 6, 7], [8]], ... "attention_mask": [[0, 1, 1], [0, 0, 1, 1], [1]], ... } >>> dataset = Dataset.from_dict(examples) >>> truncated_dataset = truncate_dataset(dataset, max_length=2) >>> truncated_dataset[:] {'input_ids': [[1, 2], [4, 5], [8]], 'attention_mask': [[0, 1], [0, 0], [1]]} ``` """ if truncation_mode not in {"keep_start", "keep_end"}: raise ValueError(f"Invalid truncation mode '{truncation_mode}'.") if map_kwargs is None: map_kwargs = {} def truncate(examples): truncated_columns = [] for column in examples.columns: if pyarrow.types.is_list(column.type) or pyarrow.types.is_large_list(column.type): if truncation_mode == "keep_start": column = pc.list_slice(column, 0, max_length) else: # keep_end column = ( pa.array([[] for _ in range(len(column))], type=column.type) if max_length == 0 else pa.array([values[-max_length:] for values in column.to_pylist()], type=column.type) ) truncated_columns.append(column) return pa.Table.from_arrays(truncated_columns, names=examples.column_names) format = _get_dataset_format(dataset) dataset = dataset.with_format("arrow") dataset = dataset.map(truncate, batched=True, **map_kwargs) dataset = dataset.with_format(**format) return dataset def is_conversational_from_value(example: dict[str, Any]) -> bool: r""" Check if the example is in a conversational format (from/value). Note that this format isn't recommended. Prefer the ChatML format (role/content) Args: example (`dict[str, Any]`): A single data entry of a dataset. The example can have different keys depending on the dataset type. Returns: `bool`: `True` if the data is in a conversational Chatformat, `False` otherwise. Examples: ```python >>> example = {"conversations": [{"from": "user", "value": "What color is the sky?"}]} >>> is_conversational_from_value(example) True >>> example = {"conversations": [{"role": "user", "content": "What color is the sky?"}]} >>> is_conversational_from_value(example) False >>> example = {"conversations": "The sky is"} >>> is_conversational_from_value(example) False ``` """ maybe_messages = example.get("conversations") # It must be a list of messages if isinstance(maybe_messages, list): maybe_message = maybe_messages[0] # Each message must a list of dictionaries with keys "from" and "value" if isinstance(maybe_message, dict) and "from" in maybe_message and "value" in maybe_message: return True return False def maybe_convert_to_chatml(example: dict[str, list]) -> dict[str, list]: """ Convert a conversational dataset with fields `from` and `value` to ChatML format. This function modifies conversational data to align with OpenAI's ChatML format: - Replaces the key `"from"` with `"role"` in message dictionaries. - Replaces the key `"value"` with `"content"` in message dictionaries. - Renames `"conversations"` to `"messages"` for consistency with ChatML. Args: example (`dict[str, list]`): A single data entry containing a list of messages. Returns: `dict[str, list]`: Example reformatted to ChatML style. Example: ```python >>> from trl import maybe_convert_to_chatml >>> example = { ... "conversations": [ ... {"from": "user", "value": "What color is the sky?"}, ... {"from": "assistant", "value": "It is blue."}, ... ] ... } >>> maybe_convert_to_chatml(example) {'messages': [{'role': 'user', 'content': 'What color is the sky?'}, {'role': 'assistant', 'content': 'It is blue.'}]} ``` """ # List of possible keys containing message lists for key in ["prompt", "completion", "chosen", "rejected", "messages", "conversations"]: if key in example and isinstance(example[key], list): messages = example[key] for message in messages: if isinstance(message, dict): if "from" in message: message["role"] = message.pop("from") if "value" in message: message["content"] = message.pop("value") # Rename "conversations" to "messages" if "conversations" in example: example["messages"] = example.pop("conversations") return example ================================================ FILE: trl/experimental/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Experimental submodule for TRL. This submodule contains unstable or incubating features. Anything here may change (or be removed) in any release without deprecation. Use at your own risk. To silence this notice set environment variable TRL_EXPERIMENTAL_SILENCE=1. """ import os import warnings from ..import_utils import TRLExperimentalWarning if not os.environ.get("TRL_EXPERIMENTAL_SILENCE"): warnings.warn( "You are importing from 'trl.experimental'. APIs here are unstable and may change or be removed without " "notice. Silence this warning by setting environment variable TRL_EXPERIMENTAL_SILENCE=1.", TRLExperimentalWarning, stacklevel=2, ) ================================================ FILE: trl/experimental/async_grpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .async_grpo_config import AsyncGRPOConfig from .async_grpo_trainer import AsyncGRPOTrainer ================================================ FILE: trl/experimental/async_grpo/async_grpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from trl.trainer.base_config import _BaseConfig @dataclass class AsyncGRPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`AsyncGRPOTrainer`]. This class includes only the parameters that are specific to asynchronous GRPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Parameters: > Parameters that control generation num_generations (`int`, *optional*, defaults to `8`): Number of generations per prompt to sample. max_completion_length (`int`, *optional*, defaults to `2048`): Maximum number of tokens to generate per completion. temperature (`float`, *optional*, defaults to `1.0`): Temperature for sampling. The higher the temperature, the more random the completions. chat_template_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to the `apply_chat_template` function when generating completions. max_tool_calling_iterations (`int`, *optional*): Maximum number of tool-calling turns when training an agent. If `None`, there is no limit and generation stops when the model generates a response turn with no tool calls or when the total response length reaches `max_completion_length`. > Parameters that control the vLLM server vllm_server_base_url (`str`, *optional*, defaults to `"http://localhost:8000"`): Base URL of the vLLM server used for generation (e.g., `"http://localhost:8000"`). vllm_server_timeout (`float`, *optional*, defaults to `240.0`): Total timeout duration in seconds to wait for the vLLM server to be ready. request_timeout (`int`, *optional*, defaults to `600`): Timeout in seconds for individual HTTP requests to the vLLM server. > Parameters that control the training epsilon (`float`, *optional*, defaults to `0.2`): Lower-bound epsilon value for clipping. epsilon_high (`float`, *optional*, defaults to `0.2`): Upper-bound epsilon value for clipping. > Parameters that control the async rollout pipeline max_inflight_tasks (`int`, *optional*, defaults to `-1`): Maximum number of concurrent generation tasks sent to the vLLM server. Defaults to `-1` (auto), which sets it to `max_staleness * per_device_train_batch_size * gradient_accumulation_steps * num_processes`. If using tool-use environments, you may want to set this manually based on how many parallel environments you can run. max_staleness (`int`, *optional*, defaults to `4`): Maximum number of weight update steps a rollout sample can lag behind the current model version before being discarded. queue_maxsize (`int`, *optional*, defaults to `1024`): Maximum number of rollout samples to buffer in the rollout queue. weight_sync_steps (`int`, *optional*, defaults to `1`): Number of training steps between weight synchronizations to the vLLM server. > Parameters that control the logging log_completions (`bool`, *optional*, defaults to `False`): Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. num_completions_to_print (`int`, *optional*, defaults to `3`): Number of completions to print when `log_completions=True`. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) logging_steps: float = field( default=1, metadata={ "help": "Log every X update steps. Should be an integer or a float in range `[0,1)`. If smaller than 1, " "will be interpreted as ratio of total training steps." }, ) # Parameters that control generation num_generations: int = field( default=8, metadata={"help": "Number of generations per prompt to sample."}, ) max_completion_length: int = field( default=2048, metadata={"help": "Maximum number of tokens to generate per completion."}, ) temperature: float = field( default=1.0, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) chat_template_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to the `apply_chat_template` function when generating " "completions." }, ) max_tool_calling_iterations: int | None = field( default=None, metadata={ "help": "Maximum number of tool-calling turns when training an agent. If `None`, there is no limit and " "generation stops when the model generates a response turn with no tool calls or when the total response " "length reaches `max_completion_length`." }, ) # Parameters that control the vLLM server vllm_server_base_url: str = field( default="http://localhost:8000", metadata={"help": "Base URL of the vLLM server used for generation (e.g., 'http://localhost:8000')."}, ) vllm_server_timeout: float = field( default=240.0, metadata={ "help": "Total timeout duration in seconds to wait for the vLLM server to be ready. If the server is not " "up after the timeout, a `TimeoutError` is raised." }, ) request_timeout: int = field( default=600, metadata={"help": "Timeout in seconds for individual HTTP requests to the vLLM server."}, ) # Parameters that control the training epsilon: float = field( default=0.2, metadata={"help": "Lower-bound epsilon value for clipping."}, ) epsilon_high: float = field( default=0.2, metadata={"help": "Upper-bound epsilon value for clipping."}, ) # Parameters that control the async rollout pipeline max_inflight_tasks: int = field( default=-1, metadata={ "help": "Maximum number of concurrent generation tasks sent to the vLLM server. Defaults to -1 (auto), " "which sets it to `max_staleness * per_device_train_batch_size * gradient_accumulation_steps * " "num_processes`. Generating more samples than this is wasteful since they will be discarded as stale " "before the trainer can consume them. If using tool-use environments, you may want to set this manually " "based on how many parallel environments you can run." }, ) max_staleness: int = field( default=4, metadata={ "help": "Maximum number of weight update steps a rollout sample can lag behind the current model version " "before being discarded." }, ) queue_maxsize: int = field( default=1024, metadata={"help": "Maximum number of rollout samples to buffer in the rollout queue."}, ) weight_sync_steps: int = field( default=1, metadata={"help": "Number of training steps between weight synchronizations to the vLLM server."}, ) # Parameters that control the logging log_completions: bool = field( default=False, metadata={ "help": "Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is " "installed, it prints the sample. If `wandb` logging is enabled, it logs it to `wandb`." }, ) num_completions_to_print: int = field( default=3, metadata={"help": "Number of completions to print when `log_completions=True`."}, ) def __post_init__(self): super().__post_init__() # Accelerator config: required for the async IterableDataset-backed dataloader to work correctly. # split_batches=True and dispatch_batches=True ensure that the main process drives the dataloader # and batches are broadcast to other processes rather than each process pulling independently. if not hasattr(self, "accelerator_config") or self.accelerator_config is None: self.accelerator_config = {"split_batches": True, "dispatch_batches": True} elif isinstance(self.accelerator_config, dict): self.accelerator_config["split_batches"] = True self.accelerator_config["dispatch_batches"] = True else: self.accelerator_config.split_batches = True self.accelerator_config.dispatch_batches = True ================================================ FILE: trl/experimental/async_grpo/async_grpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import queue import textwrap import time from collections import defaultdict from collections.abc import Callable, Iterator from dataclasses import dataclass from typing import Any, Protocol import torch from accelerate.logging import get_logger from datasets import Dataset, IterableDataset from torch.distributed._tensor import DTensor from torch.utils.data import DataLoader from transformers import AutoModelForCausalLM, AutoTokenizer, PreTrainedTokenizerBase, TrainerCallback from transformers.data.data_collator import DataCollatorMixin from trl.trainer.base_trainer import _BaseTrainer from trl.trainer.utils import pad, selective_log_softmax from .async_grpo_config import AsyncGRPOConfig from .async_rollout_worker import AsyncRolloutWorker logger = get_logger(__name__) # A reward function is a callable that returns a list of floats (the rewards). The callable receives prompts, # completions, and additional arguments from the trainer (refer to the trainer's source for details). To ensure forward # compatibility, it should accept **kwargs. RewardFunc = Callable[..., list[float]] class _SupportsReset(Protocol): def reset(self, **kwargs) -> str | None: ... EnvironmentFactory = Callable[[], _SupportsReset] class RolloutWorkerProtocol(Protocol): rollout_buffer: queue.Queue def start(self) -> None: ... def stop(self) -> None: ... def pause(self) -> None: ... def resume(self) -> None: ... def send_weights(self, iterator: Iterator[tuple[str, torch.Tensor]]) -> None: ... def update_model_version(self, version: int) -> None: ... class StepIntervalCallback(TrainerCallback): """ A callback that calls a function every N optimization steps. """ def __init__(self, fn, every_n_steps: int): self.fn = fn self.every_n_steps = every_n_steps def on_step_end(self, _args, state, _control, **_kwargs): if state.global_step % self.every_n_steps == 0: self.fn() class RolloutQueueDataset(torch.utils.data.IterableDataset): def __init__(self, rollout_queue, model_version_fn, max_staleness=3, timeout=120.0): self.queue = rollout_queue self.model_version_fn = model_version_fn self.max_staleness = max_staleness self.timeout = timeout def __iter__(self): while True: t0 = time.time() qsize = self.queue.qsize() if qsize == 0: logger.info("queue empty, waiting for rollout samples...") try: sample = self.queue.get(timeout=self.timeout) except queue.Empty: logger.warning(f"Rollout queue empty for {self.timeout}s, stopping epoch") return # StopIteration ends epoch queue_wait_time_s = time.time() - t0 if queue_wait_time_s > 1.0: logger.info(f"waited {queue_wait_time_s:.1f}s for sample (qsize={self.queue.qsize()})") staleness = self.model_version_fn() - sample.model_version if staleness > self.max_staleness: logger.info(f"dropping stale sample (staleness={staleness}, max={self.max_staleness})") continue # drop stale, pull next yield { "input_ids": sample.input_ids, "completion_mask": sample.completion_mask, "old_log_probs": sample.old_log_probs, "advantage": sample.advantage, "metrics": {**sample.metrics, "queue_wait_time_s": queue_wait_time_s}, } class _EmptyIterableDataset(torch.utils.data.IterableDataset): """Placeholder for non-rank-0 processes. Never actually iterated.""" def __iter__(self): return iter([]) @dataclass class DataCollatorForRollout(DataCollatorMixin): pad_token_id: int return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: input_ids = [torch.tensor(example["input_ids"], dtype=torch.long) for example in examples] attention_mask = [torch.ones(len(ids), dtype=torch.long) for ids in input_ids] completion_mask = [torch.tensor(example["completion_mask"], dtype=torch.float32) for example in examples] old_log_probs = [torch.tensor(example["old_log_probs"], dtype=torch.float32) for example in examples] advantages = torch.tensor([example["advantage"] for example in examples], dtype=torch.float32) input_ids = pad(input_ids, padding_value=self.pad_token_id) attention_mask = pad(attention_mask, padding_value=0) completion_mask = pad(completion_mask, padding_value=0) old_log_probs = pad(old_log_probs, padding_value=0) # Total valid completion tokens across all samples in the full batch. # Repeated per sample so that DataLoaderDispatcher (dispatch_batches=True) slices correctly on dim=0 global_n_tokens = completion_mask.sum() global_n_tokens_repeated = torch.full((len(examples),), global_n_tokens.item(), dtype=torch.float32) # Convert per-sample metrics dicts to a dict of 1D tensors so that Accelerate's # recursive broadcast (dispatch_batches=True) can handle them — it traverses nested # dicts of tensors but chokes on plain Python floats. metrics_list = [example["metrics"] for example in examples] metrics = ( { key: torch.tensor([m.get(key, 0.0) for m in metrics_list], dtype=torch.float32) for key in metrics_list[0] } if metrics_list and metrics_list[0] else {} ) return { "input_ids": input_ids, "attention_mask": attention_mask, "completion_mask": completion_mask, "old_log_probs": old_log_probs, "advantages": advantages, "global_n_tokens": global_n_tokens_repeated, "metrics": metrics, } class AsyncGRPOTrainer(_BaseTrainer): """ Trainer for the Group Relative Policy Optimization (GRPO) method. This algorithm was initially proposed in the paper [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300). This trainer is the asynchronous version of GRPO, where generation is offloaded to an external vLLM server that runs asynchronously alongside training, decoupling rollout from the gradient update loop. Example: ```python from trl.experimental.async_grpo import AsyncGRPOTrainer from trl.rewards import accuracy_reward from datasets import load_dataset dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = AsyncGRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` Args: model (`str`): Model to be trained. Must be a string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForCausalLM.from_pretrained`]. The model name is also used to identify the model on the vLLM server used for generation. reward_funcs (`RewardFunc | list[RewardFunc]`): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function: The function is provided with the prompts and the generated completions, plus any additional columns in the dataset. It should return a list of rewards. Reward functions can be either synchronous or asynchronous and can also return `None` when the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returns `None` for a sample, that reward function is excluded from the reward calculation for that sample. For more details, see [Using a custom reward function](#using-a-custom-reward-function). - A list of reward functions, where each item is a reward function as described above. Rewards from all functions are summed. args ([`AsyncGRPOConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. It must include a column `"prompt"`. Any additional columns in the dataset are ignored. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). processing_class ([`~transformers.PreTrainedTokenizerBase`], *optional*): Processing class used to process the data. The padding side must be set to `"left"`. If `None`, the processing class is loaded from the model's name with [`~transformers.AutoTokenizer.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. tools (list of `Callable`, *optional*): A list of callable tool functions (sync or async) that the model can invoke during generation. Each tool should be a standard Python function with properly type-hinted arguments and return values, and a Google-style docstring describing its purpose, arguments, and return value. For more details, see: https://huggingface.co/docs/transformers/en/chat_extras#passing-tools. The model uses the function's name, type hints, and docstring to determine how to call it. Ensure that the model's chat template supports tool use and that it has been fine-tuned for tool calling. environment_factory (`EnvironmentFactory`, *optional*): A callable that creates and returns an environment instance. The environment class should define methods that can be invoked as tools during generation. Each method should comply with the same requirements as the `tools` described above. If `environment_factory` is provided, an instance of the environment is created for each generation in the batch, allowing for parallel and independent interactions. The environment must also implement a callable `reset` method that can be used to reset state between generations. The `reset` method should return either `None` or a string: when it returns a string, that string is appended to the last user message before generation. This feature is experimental and may change or be removed at any time without prior notice. """ _tag_names = ["trl", "async-grpo"] _name = "AsyncGRPO" _paper = { "title": "DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models", "id": "2402.03300", # docstyle-ignore "citation": textwrap.dedent("""\ @article{shao2024deepseekmath, title = {{DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}}, author = {Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo}, year = 2024, eprint = {arXiv:2402.03300}, } """), } def __init__( self, model: str, reward_funcs: RewardFunc | list[RewardFunc], args: AsyncGRPOConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, processing_class: PreTrainedTokenizerBase | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), tools: list[Callable] | None = None, environment_factory: EnvironmentFactory | None = None, rollout_worker: RolloutWorkerProtocol | None = None, ): self.args = args or AsyncGRPOConfig() # Training arguments self.epsilon_low = self.args.epsilon self.epsilon_high = self.args.epsilon_high self.temperature = self.args.temperature # Model model_name = model model = AutoModelForCausalLM.from_pretrained(model, device_map=None, dtype=torch.bfloat16) # Processing class if processing_class is None: processing_class = AutoTokenizer.from_pretrained(model_name) if processing_class.pad_token is None: processing_class.pad_token = processing_class.eos_token # Reward functions if not isinstance(reward_funcs, list): reward_funcs = [reward_funcs] # Initialize the Trainer super().__init__( model=model, args=self.args, train_dataset=train_dataset, processing_class=processing_class, callbacks=callbacks, optimizers=optimizers, compute_loss_func="non-None value to disable scaling", ) # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Infer max_steps from dataset size when not explicitly set. This must happen after super().__init__() # so that self.accelerator.num_processes is available for the correct calculation. samples_per_step = ( self.args.per_device_train_batch_size * self.args.gradient_accumulation_steps * self.accelerator.num_processes ) if self.args.max_steps <= 0 and train_dataset is not None and hasattr(train_dataset, "__len__"): samples_per_epoch = len(train_dataset) * self.args.num_generations self.args.max_steps = int(self.args.num_train_epochs * samples_per_epoch / samples_per_step) # Infer max_inflight_tasks when not explicitly set. Generating more samples than the trainer can consume # before they become stale is wasteful. The useful upper bound is max_staleness * samples_per_step. if self.args.max_inflight_tasks < 0: self.args.max_inflight_tasks = self.args.max_staleness * samples_per_step logger.info( f"max_inflight_tasks set to {self.args.max_inflight_tasks} " f"(max_staleness={self.args.max_staleness} × samples_per_step={samples_per_step})" ) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._train_tokens_start_time = None self.model_version = 0 # Create worker and queue on rank 0 if self.accelerator.is_main_process: if self.train_dataset is None: raise ValueError("train_dataset is required for AsyncGRPOTrainer") if rollout_worker is not None: # Use the injected worker (e.g. a stub in tests). The queue is owned by the worker. self.rollout_worker = rollout_worker else: # Collect weight metadata once — names/dtypes/shapes are fixed for the lifetime of training. # DTensor.shape returns the global shape without triggering any all-gather. weight_names, weight_dtype_names, weight_shapes = [], [], [] for name, param in model.named_parameters(): # DDP/FSDP1 wrapping, avoids vllm module not exist error name = name.removeprefix("module.") weight_names.append(name) weight_dtype_names.append(str(param.dtype).split(".")[-1]) weight_shapes.append(list(param.shape)) self.rollout_worker = AsyncRolloutWorker( model_name=model_name, dataset=train_dataset, reward_funcs=reward_funcs, tools=tools, environment_factory=environment_factory, num_generations=self.args.num_generations, max_inflight_tasks=self.args.max_inflight_tasks, queue_maxsize=self.args.queue_maxsize, vllm_server_url=self.args.vllm_server_base_url, max_tokens=self.args.max_completion_length, temperature=self.args.temperature, request_timeout=self.args.request_timeout, server_timeout=self.args.vllm_server_timeout, chat_template_kwargs=self.args.chat_template_kwargs, max_tool_calling_iterations=self.args.max_tool_calling_iterations, log_completions=self.args.log_completions, num_completions_to_print=self.args.num_completions_to_print, weight_names=weight_names, weight_dtype_names=weight_dtype_names, weight_shapes=weight_shapes, ) self.rollout_queue = self.rollout_worker.rollout_buffer else: self.rollout_queue = None self.rollout_worker = None # Add callbacks self.add_callback(StepIntervalCallback(self._sync_weight, self.args.weight_sync_steps)) def get_train_dataloader(self) -> DataLoader: if self.accelerator.is_main_process: dataset = RolloutQueueDataset( rollout_queue=self.rollout_queue, model_version_fn=lambda: self.model_version, max_staleness=self.args.max_staleness, timeout=self.args.vllm_server_timeout, ) else: dataset = _EmptyIterableDataset() return self.accelerator.prepare( DataLoader( dataset, batch_size=self.args.per_device_train_batch_size * self.accelerator.num_processes, collate_fn=DataCollatorForRollout(self.processing_class.pad_token_id), num_workers=0, # MUST be 0 ) # NOTE(@aminediro): # dispatch_batches = True for DataLoader whose underlying dataset is an IterableDataset # dataloader prepared by the Accelerator is only iterated through on the main process a ) def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). In AsyncGRPOTrainer, we need additional columns ("completion_mask", "old_log_probs", # "advantages", "global_n_tokens") to compute the loss, hence the override. if self._signature_columns is None: self._signature_columns = [ "input_ids", "attention_mask", "completion_mask", "old_log_probs", "advantages", "global_n_tokens", "metrics", ] def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] completion_mask = inputs["completion_mask"] old_log_probs = inputs["old_log_probs"] advantages = inputs["advantages"] # The collator pads to the global batch max length (across all ranks). After DataLoaderDispatcher slices and # sends rows to each rank, the local slice is still padded to that global max. Truncate to the longest real # sequence in this rank's slice so we don't run the forward pass over pure-padding columns. local_max_len = attention_mask.sum(dim=1).max() input_ids = input_ids[:, :local_max_len] attention_mask = attention_mask[:, :local_max_len] completion_mask = completion_mask[:, :local_max_len] old_log_probs = old_log_probs[:, :local_max_len] forward_start = time.time() outputs = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False) self._last_forward_time_s = time.time() - forward_start logits = outputs.logits[:, :-1, :] targets = input_ids[:, 1:] logits.div_(self.temperature) log_probs = selective_log_softmax(logits, targets) completion_mask = completion_mask[:, 1:] old_log_probs = old_log_probs[:, 1:] advantages = advantages.unsqueeze(1) log_ratio = log_probs - old_log_probs ratio = torch.exp(log_ratio) clipped = torch.clamp(ratio, 1 - self.epsilon_low, 1 + self.epsilon_high) per_token_loss = -torch.min(ratio * advantages, clipped * advantages) # DDP/FSDP averages gradients across ranks (world_size). # To get correct per-token normalization we scale by 1/tokens_per_rank # = world_size / global_n_tokens, so after DDP averaging the effective loss = (per_token_loss * completion_mask).sum() global_n_tokens = inputs["global_n_tokens"][0] world_size = self.accelerator.num_processes tokens_per_rank = (global_n_tokens / world_size).clamp(min=1.0) loss = loss / tokens_per_rank.to(torch.float32) # For DAPO, we would scale like this instead: # loss = loss / max(per_token_loss.size(0), 1) loss = loss / self.current_gradient_accumulation_steps with torch.no_grad(): valid_mask = completion_mask > 0 local_count = valid_mask.sum().float() local_ratio_sum = ( ratio[valid_mask].sum() if valid_mask.any() else torch.zeros((), device=completion_mask.device) ) # Approx KL: http://joschu.net/blog/kl-approx.html local_kl_sum = ( ((ratio[valid_mask] - 1) - log_ratio[valid_mask]).sum() if valid_mask.any() else torch.zeros((), device=completion_mask.device) ) probs = torch.softmax(logits, dim=-1) log_p = torch.log_softmax(logits, dim=-1) entropy = -torch.sum(probs * log_p, dim=-1) local_entropy_sum = ( entropy[valid_mask].sum() if valid_mask.any() else torch.zeros((), device=completion_mask.device) ) clipped = (ratio < 1 - self.epsilon_low) | (ratio > 1 + self.epsilon_high) local_clip_sum = ( clipped[valid_mask].float().sum() if valid_mask.any() else torch.zeros((), device=completion_mask.device) ) # Batch all-reduce: [ratio_sum, kl_sum, entropy_sum, clip_sum, count] stats = torch.stack([local_ratio_sum, local_kl_sum, local_entropy_sum, local_clip_sum, local_count]) stats = self.accelerator.reduce(stats, reduction="sum") global_ratio_sum, global_kl_sum, global_entropy_sum, global_clip_sum, global_count = stats.unbind(0) self._metrics["train"]["ratio"].append((global_ratio_sum / global_count).item()) self._metrics["train"]["kl"].append((global_kl_sum / global_count).item()) self._metrics["train"]["entropy"].append((global_entropy_sum / global_count).item()) self._metrics["train"]["clip_ratio"].append((global_clip_sum / global_count).item()) # Logging metrics from the rollout worker (reward, reward_std, etc.). # inputs["metrics"] is a dict of 1D tensors keyed by metric name. sample_metrics = inputs["metrics"] # dict[str, Tensor(shape=[B_local])] keys = list(sample_metrics.keys()) device = completion_mask.device n_samples = torch.tensor(completion_mask.shape[0], dtype=torch.float32, device=device) if keys: local_sums = torch.stack([sample_metrics[k].to(device).sum() for k in keys]) stats = torch.cat([local_sums, n_samples.unsqueeze(0)]) stats = self.accelerator.reduce(stats, reduction="sum") global_sums, global_n_samples = stats[:-1], stats[-1] for k, global_sum in zip(keys, global_sums, strict=True): self._metrics["train"][k].append((global_sum / global_n_samples).item()) completion_length = completion_mask.sum(dim=1).float() length_stats = torch.stack([completion_length.sum(), n_samples]) length_stats = self.accelerator.reduce(length_stats, reduction="sum") self._metrics["train"]["completions/mean_length"].append((length_stats[0] / length_stats[1]).item()) # Training throughput: completion tokens consumed by this training step per second. now = time.time() if self._train_tokens_start_time is not None: train_elapsed = now - self._train_tokens_start_time if train_elapsed > 0: self._metrics["train"]["training_tok/s"].append(global_n_tokens.item() / train_elapsed) self._train_tokens_start_time = now self._metrics["train"]["forward_time_s"].append(self._last_forward_time_s) # NOTE: in dynamic mbs setup, we would need to agg across DP ranks. self._metrics["train"]["train_seq_len"].append(float(local_max_len)) return loss def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() def _streaming_iter(self): # Iterate parameters one at a time. For FSDP2 (DTensor), full_tensor() all-gathers just this parameter across # FSDP ranks, then frees it once the generator advances — avoiding materializing the full model in memory. for name, param in self.model.named_parameters(): name = name.removeprefix("module.") # DDP/FSDP1 wrapping full = param.full_tensor() if isinstance(param, DTensor) else param.detach() yield name, full def _sync_weight(self): t0 = time.time() logger.info("Weight sync: pausing vLLM...") if self.accelerator.is_main_process and self.rollout_worker: self.rollout_worker.pause() t_pause = time.time() logger.info(f"Weight sync: pause took {t_pause - t0:.1f}s, waiting for all ranks...") self.accelerator.wait_for_everyone() t_barrier = time.time() logger.info(f"Weight sync: transferring weights... (barrier took {t_barrier - t_pause:.1f}s)") if self.accelerator.is_main_process and self.rollout_worker: self.rollout_worker.send_weights(self._streaming_iter()) else: # Non-rank-0 processes must still participate in full_tensor() collectives for FSDP2. for _ in self._streaming_iter(): pass t_transfer = time.time() self.accelerator.wait_for_everyone() logger.info(f"Weight sync: resuming vLLM... (transfer took {t_transfer - t_barrier:.1f}s)") if self.accelerator.is_main_process and self.rollout_worker: self.rollout_worker.resume() self.model_version += 1 self.rollout_worker.update_model_version(self.model_version) weight_sync_time_s = time.time() - t0 self._metrics["train"]["weight_sync_time_s"].append(weight_sync_time_s) logger.info(f"Weight sync: done. Total {weight_sync_time_s:.1f}s") def _inner_training_loop(self, *args, **kwargs): # Start the rollout worker here (not in __init__) so that checkpoint loading in Trainer.train() # has already restored the model weights. The sequence is: start worker thread → wait for NCCL # init → sync weights to vLLM → begin generation. This ensures vLLM always uses the current # policy before producing any samples (matters for resumed runs, harmless for fresh ones). self._sync_weight() if self.accelerator.is_main_process and self.rollout_worker: self.rollout_worker.start() try: return super()._inner_training_loop(*args, **kwargs) finally: if self.accelerator.is_main_process and self.rollout_worker: self.rollout_worker.stop() ================================================ FILE: trl/experimental/async_grpo/async_rollout_worker.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import inspect import queue import threading import time from collections.abc import Callable, Iterator from dataclasses import dataclass from typing import Any, TypeAlias import aiohttp import numpy as np import requests from accelerate.logging import get_logger from datasets import Dataset from transformers import AutoTokenizer from trl.chat_template_utils import add_response_schema, get_training_chat_template, parse_response from trl.import_utils import is_vllm_available from trl.trainer.utils import print_prompt_completions_sample if is_vllm_available(min_version="0.17.1"): from vllm.distributed.weight_transfer.nccl_engine import NCCLTrainerSendWeightsArgs, NCCLWeightTransferEngine from vllm.utils.network_utils import get_ip, get_open_port logger = get_logger(__name__) Messages: TypeAlias = list[dict[str, str]] @dataclass(slots=True) class RolloutGroup: """Single GRPO group for one prompt with multiple completions.""" prompt: Messages prompt_ids: list[int] reward_kwargs: dict[str, list[Any]] completions: list[Messages] completions_ids: list[list[int]] completions_logprobs: list[list[float]] tool_mask: list[list[int]] tool_call_counts: list[int] tool_failure_counts: list[int] model_version: int queued_at: float = 0.0 @dataclass(slots=True) class RolloutSample: prompt: Messages completion: Messages input_ids: list[int] completion_mask: list[int] old_log_probs: list[float] advantage: float model_version: int metrics: dict[str, float] # logging metadata only, not used in loss computation class AsyncRolloutWorker: """ Minimal asynchronous actor worker structure. Loop: generate groups -> score groups -> push samples -> repeat """ def __init__( self, model_name: str, dataset: Dataset, reward_funcs: list[Callable[..., list[float]]], tools: list[Callable] | None = None, environment_factory: Callable[[], object] | None = None, num_generations: int = 8, max_inflight_tasks: int = 128, queue_maxsize: int = 0, vllm_server_url: str = "http://localhost:8000", max_tokens: int = 32, temperature: float = 1.0, request_timeout: int = 120, server_timeout: float = 240.0, chat_template_kwargs: dict[str, Any] | None = None, max_tool_calling_iterations: int | None = None, log_completions: bool = False, num_completions_to_print: int = 3, weight_names: list[str] | None = None, weight_dtype_names: list[str] | None = None, weight_shapes: list[list[int]] | None = None, ): if not is_vllm_available(min_version="0.17.1"): raise ImportError( "vLLM >= 0.17.1 is required to use AsyncRolloutWorker. Install it with: pip install 'vllm>=0.17.1'" ) self.model_name = model_name self.max_tool_calling_iterations = max_tool_calling_iterations self.dataset = dataset self._dataset_iter = iter(dataset) self.rollout_buffer: queue.Queue[RolloutSample] = queue.Queue(maxsize=queue_maxsize) self._loop: asyncio.AbstractEventLoop | None = None self._stop_event: asyncio.Event | None = None self._weight_update_info = { "names": weight_names, "dtype_names": weight_dtype_names, "shapes": weight_shapes, "packed": True, "is_checkpoint_format": True, } self.reward_funcs = reward_funcs self.reward_func_names = [f.__name__ for f in reward_funcs] self.num_generations = num_generations self.max_inflight_tasks = max_inflight_tasks self.environments = None environment_methods = [[] for _ in range(self.max_inflight_tasks)] if environment_factory is not None: self.environments = [environment_factory() for _ in range(self.max_inflight_tasks)] for i, environment in enumerate(self.environments): has_reset = False for name, member in inspect.getmembers(environment, predicate=inspect.ismethod): if name == "reset": has_reset = True elif not name.startswith("_"): environment_methods[i].append(member) if not has_reset: raise ValueError( "Each environment instance returned by `environment_factory` must define `reset`." ) base_tools = tools or [] self._sync_tool_dicts = [{} for _ in range(self.max_inflight_tasks)] for i in range(self.max_inflight_tasks): for tool in base_tools + (environment_methods[i] if self.environments is not None else []): if inspect.iscoroutinefunction(tool): raise ValueError("Asynchronous tools are not supported in AsyncRolloutWorker yet.") self._sync_tool_dicts[i][tool.__name__] = tool self.tools = base_tools + (environment_methods[0] if self.environments is not None else []) self.vllm_server_url = vllm_server_url.rstrip("/") self.model_update_group = None self.max_tokens = max_tokens self.temperature = temperature self.request_timeout = request_timeout self.server_timeout = server_timeout self.chat_template_kwargs = chat_template_kwargs or {} self.log_completions = log_completions self.num_completions_to_print = num_completions_to_print self.tokenizer = AutoTokenizer.from_pretrained(model_name) self.tokenizer = add_response_schema(self.tokenizer) self.chat_template = get_training_chat_template(self.tokenizer) self._groups_to_score: asyncio.Queue[RolloutGroup | None] = asyncio.Queue(maxsize=16) self._total_completion_tokens = 0 self._total_groups_scored = 0 self._generation_start_time: float | None = None self.model_version = 0 self.session = None # Wait for the vLLM server and initialize NCCL weight transfer. self._wait_for_server_ready_sync(timeout_s=self.server_timeout) self._init_weight_transfer() def _wait_for_server_ready_sync(self, timeout_s: float = 240.0, poll_interval_s: float = 2.0) -> None: """Block until the vLLM server is healthy.""" logger.info(f"Waiting for vLLM server at {self.vllm_server_url} ...") start = time.time() while True: elapsed = time.time() - start try: response = requests.get(f"{self.vllm_server_url}/health", timeout=5) if response.status_code == 200: logger.info(f"vLLM server ready after {elapsed:.1f}s") return except (requests.ConnectionError, requests.Timeout, OSError): pass if elapsed >= timeout_s: raise TimeoutError( f"Timed out after {timeout_s:.0f}s waiting for vLLM server at {self.vllm_server_url}. " "Make sure the vLLM server is running and reachable. If the server needs more time to load " "the model, increase `vllm_server_timeout` in your AsyncGRPOConfig." ) if int(elapsed) % 10 < poll_interval_s: logger.info(f"Still waiting for vLLM server... ({elapsed:.0f}s)") time.sleep(poll_interval_s) def _init_weight_transfer(self) -> None: response = requests.get(f"{self.vllm_server_url}/get_world_size") inference_world_size = response.json()["world_size"] world_size = inference_world_size + 1 master_address = get_ip() master_port = get_open_port() init_info = { "master_address": master_address, "master_port": master_port, "rank_offset": 1, "world_size": world_size, } t_init = threading.Thread( target=requests.post, args=(f"{self.vllm_server_url}/init_weight_transfer_engine",), kwargs={"json": {"init_info": init_info}, "timeout": 120}, ) t_init.start() self.model_update_group = NCCLWeightTransferEngine.trainer_init( { "master_address": master_address, "master_port": master_port, "world_size": world_size, } ) t_init.join() logger.info("Init weight sync group with vLLM") def update_model_version(self, model_version: int): self.model_version = model_version async def _run_loops(self, stop_event: asyncio.Event) -> None: async with aiohttp.ClientSession() as session: self.session = session logger.info( f"vllm worker started: num_generations={self.num_generations}, max_inflight_tasks={self.max_inflight_tasks}" ) await asyncio.gather( asyncio.create_task(self._generate_loop(stop_event=stop_event)), asyncio.create_task(self._score_loop(stop_event=stop_event)), ) def start(self) -> None: thread = threading.Thread(target=self._run, daemon=True) thread.start() def stop(self) -> None: logger.info("Stopping worker thread...") if self._loop and self._loop.is_running(): try: self._loop.call_soon_threadsafe(self._stop_event.set) except RuntimeError: pass def _run(self) -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) self._loop = loop self._stop_event = asyncio.Event() try: loop.run_until_complete(self._run_loops(stop_event=self._stop_event)) except Exception as e: logger.exception(f"Worker thread failed: {e}") raise finally: loop.close() def pause(self) -> None: t0 = time.time() requests.post(f"{self.vllm_server_url}/pause", params={"mode": "keep"}) logger.debug(f"[weight_sync] pause HTTP took {time.time() - t0:.1f}s") def resume(self) -> None: t0 = time.time() requests.post(f"{self.vllm_server_url}/resume") logger.debug(f"[weight_sync] resume HTTP took {time.time() - t0:.1f}s") def send_weights(self, iterator) -> None: if self.model_update_group is None: return t0 = time.time() t_update = threading.Thread( target=requests.post, args=(f"{self.vllm_server_url}/update_weights",), kwargs={"json": {"update_info": self._weight_update_info}, "timeout": 1800}, ) t_update.start() logger.debug(f"[weight_sync] /update_weights POST sent ({time.time() - t0:.1f}s)") t_nccl = time.time() NCCLWeightTransferEngine.trainer_send_weights( iterator=iterator, trainer_args=NCCLTrainerSendWeightsArgs(group=self.model_update_group, packed=True), ) logger.debug(f"[weight_sync] NCCL transfer took {time.time() - t_nccl:.1f}s") t_join = time.time() t_update.join() logger.debug( f"[weight_sync] /update_weights join took {time.time() - t_join:.1f}s (total send_weights: {time.time() - t0:.1f}s)" ) async def _generate_loop(self, stop_event: asyncio.Event) -> None: pending_groups: dict[int, RolloutGroup] = {} pending_completed: dict[int, int] = {} inflight_tasks: dict[asyncio.Task, tuple[int, int]] = {} free_slots = set(range(self.max_inflight_tasks)) work_iter = self._repeat_iterator() self._generation_start_time = time.monotonic() try: while True: while free_slots and not stop_event.is_set(): group_id, row = next(work_iter) if group_id not in pending_groups: prompt = row["prompt"] prompt_ids = self.tokenizer.apply_chat_template( prompt, return_dict=False, add_generation_prompt=True, tools=self.tools, chat_template=self.chat_template, **self.chat_template_kwargs, ) reward_kwargs = { key: [row[key]] * self.num_generations for key in row if key not in {"prompt", "completion", "completion_ids"} } pending_groups[group_id] = RolloutGroup( prompt=prompt, prompt_ids=prompt_ids, reward_kwargs=reward_kwargs, completions=[], completions_ids=[], completions_logprobs=[], tool_mask=[], tool_call_counts=[], tool_failure_counts=[], model_version=self.model_version, ) pending_completed[group_id] = 0 logger.debug(f"Started group {group_id}; pending_groups={len(pending_groups)}") slot = free_slots.pop() if self.environments is not None: # Current assumption: reset side effects matter, return value is ignored. self.environments[slot].reset(**row) logger.info(f"[slot] assigned slot={slot} group={group_id} free_after={len(free_slots)}") task = asyncio.create_task( self._generate_one(pending_groups[group_id].prompt, tool_dict=self._sync_tool_dicts[slot]) ) inflight_tasks[task] = (group_id, slot) if not inflight_tasks: if stop_event.is_set(): return await asyncio.sleep(0.01) continue done, _ = await asyncio.wait(inflight_tasks, return_when=asyncio.FIRST_COMPLETED, timeout=0.1) if not done: if not free_slots: logger.debug( f"[generate] all {self.max_inflight_tasks} slots busy, " f"pending_groups={len(pending_groups)}, waiting for completions..." ) continue for task in done: group_id, slot = inflight_tasks.pop(task) free_slots.add(slot) logger.debug(f"[slot] freed slot={slot} group={group_id} free_after={len(free_slots)}") if task.exception() is not None: raise task.exception() ( completion, completion_ids, completion_logprobs, tool_mask, tool_call_count, tool_failure_count, ) = task.result() group = pending_groups[group_id] group.completions.append(completion) group.completions_ids.append(completion_ids) group.completions_logprobs.append(completion_logprobs) group.tool_mask.append(tool_mask) group.tool_call_counts.append(tool_call_count) group.tool_failure_counts.append(tool_failure_count) # TODO: move this in generation task, shouldn't matter but is correct self._total_completion_tokens += sum(tool_mask) pending_completed[group_id] += 1 if pending_completed[group_id] == self.num_generations: group.queued_at = time.monotonic() while True: try: self._groups_to_score.put_nowait(group) break except asyncio.QueueFull: if stop_event.is_set(): return await asyncio.sleep(0.1) logger.debug(f"Group {group_id} complete; queued_for_scoring={self._groups_to_score.qsize()}") del pending_groups[group_id] del pending_completed[group_id] finally: for task in inflight_tasks: task.cancel() if inflight_tasks: await asyncio.gather(*inflight_tasks, return_exceptions=True) # Use put_nowait: if the queue is full at shutdown, skip the sentinel — # _score_loop will exit via stop_event check in its outer loop. try: self._groups_to_score.put_nowait(None) except asyncio.QueueFull: pass def _compute_rollout_metrics(self, samples: list[RolloutSample], scoring_time: float, wait_scoring: float) -> None: assert self._generation_start_time is not None, "generation_start_time init in run()" elapsed = time.monotonic() - self._generation_start_time generation_tok_per_sec = self._total_completion_tokens / elapsed if elapsed > 0 else 0.0 scoring_time_ms = scoring_time * 1000 wait_scoring_ms = wait_scoring * 1000 for sample in samples: sample.metrics["generation_tok_per_s"] = generation_tok_per_sec sample.metrics["scoring_time_ms"] = scoring_time_ms sample.metrics["wait_scoring_ms"] = wait_scoring_ms sample.metrics["buffer_qsize"] = self.rollout_buffer.qsize() logger.info( f"[inference] total_completion_tokens={self._total_completion_tokens}, " f"generation_tok/s={generation_tok_per_sec:.1f}, scoring_time={scoring_time_ms:.1f}ms, " f"wait_scoring={wait_scoring_ms:.1f}ms" ) async def _score_loop(self, stop_event: asyncio.Event) -> None: while not stop_event.is_set(): t_wait = time.monotonic() try: group = await asyncio.wait_for(self._groups_to_score.get(), timeout=0.5) except asyncio.TimeoutError: continue if group is None: return score_queue_wait = time.monotonic() - t_wait wait_scoring = time.monotonic() - group.queued_at if score_queue_wait > 0.5: logger.info(f"[score] waited {score_queue_wait:.1f}s for a group to score") t0 = time.monotonic() samples = await self._score_group(group) scoring_time = time.monotonic() - t0 logger.info( f"[score] scored {len(samples)} samples in {scoring_time:.2f}s, " f"buffer_qsize={self.rollout_buffer.qsize()}" ) self._compute_rollout_metrics(samples, scoring_time, wait_scoring) if self.log_completions and samples: print_prompt_completions_sample( prompts=[s.prompt for s in samples], completions=[s.completion for s in samples], rewards={"reward": [s.metrics["reward"] for s in samples]}, advantages=[s.advantage for s in samples], step=self._total_groups_scored, num_samples=self.num_completions_to_print, ) self._total_groups_scored += 1 for sample in samples: while True: try: self.rollout_buffer.put_nowait(sample) break except queue.Full: if stop_event.is_set(): return # Wait for trainer to consume loop logger.info( f"[score] rollout buffer full (maxsize={self.rollout_buffer.maxsize}), waiting for trainer to consume..." ) await asyncio.sleep(0.1) logger.debug( f"Scored group with {len(samples)} samples; rollout_buffer_qsize={self.rollout_buffer.qsize()}" ) def _repeat_iterator(self) -> Iterator[tuple[int, dict[str, Any]]]: group_id = 0 while True: try: row = next(self._dataset_iter) except StopIteration: self._dataset_iter = iter(self.dataset) row = next(self._dataset_iter) for _ in range(self.num_generations): yield group_id, row group_id += 1 async def _generate_one( self, prompt: Messages, tool_dict: dict[str, Callable] ) -> tuple[list[dict[str, str]], list[int], list[float], list[int], int, int]: completion, completion_ids, completion_logprobs, tool_mask = [], [], [], [] tool_call_count = 0 tool_failure_count = 0 iteration_num = 0 max_iterations = self.max_tool_calling_iterations prompt_ids = self.tokenizer.apply_chat_template( prompt, return_dict=False, add_generation_prompt=True, tools=self.tools, chat_template=self.chat_template, **self.chat_template_kwargs, ) while True: turn_ids, turn_logprobs = await self._generate_one_turn(prompt_ids) assistant_message = parse_response(self.tokenizer, turn_ids) completion.append(assistant_message) completion_ids.extend(turn_ids) completion_logprobs.extend(turn_logprobs) tool_mask.extend([1] * len(turn_ids)) tool_calls = assistant_message.get("tool_calls") if tool_calls is None or (max_iterations is not None and iteration_num >= max_iterations): return completion, completion_ids, completion_logprobs, tool_mask, tool_call_count, tool_failure_count tool_messages, n_calls, n_failures = self._execute_tool_calls(tool_calls, tool_dict) tool_call_count += n_calls tool_failure_count += n_failures completion.extend(tool_messages) tool_suffix_ids = self._build_messages_suffix_ids(tool_messages) completion_ids.extend(tool_suffix_ids) completion_logprobs.extend([0.0] * len(tool_suffix_ids)) tool_mask.extend([0] * len(tool_suffix_ids)) prompt_ids = prompt_ids + turn_ids + tool_suffix_ids iteration_num += 1 def _build_messages_suffix_ids(self, messages: list[dict[str, Any]]) -> list[int]: template_messages = [ {"role": "user", "content": ""}, {"role": "assistant", "content": ""}, ] prefix_ids = self.tokenizer.apply_chat_template( template_messages, return_dict=False, tools=self.tools, chat_template=self.chat_template, **self.chat_template_kwargs, ) prefix_and_messages_ids = self.tokenizer.apply_chat_template( template_messages + messages, return_dict=False, chat_template=self.chat_template, add_generation_prompt=True, tools=self.tools, **self.chat_template_kwargs, ) prefix_len = len(prefix_ids) if prefix_and_messages_ids[:prefix_len] != prefix_ids: raise ValueError("Failed to construct message suffix in token space.") return prefix_and_messages_ids[prefix_len:] def _execute_tool_calls( self, tool_calls: list[dict[str, Any]], tool_dict: dict[str, Callable] ) -> tuple[list[dict[str, str]], int, int]: tool_messages = [] n_calls = 0 n_failures = 0 for tool_call in tool_calls: n_calls += 1 function = tool_call["function"] name = function["name"] try: arguments = function.get("arguments", {}) result = tool_dict[name](**arguments) except Exception as error: n_failures += 1 result = {"error": str(error)} tool_messages.append({"role": "tool", "name": name, "content": str(result)}) return tool_messages, n_calls, n_failures async def _generate_one_turn(self, prompt_ids: list[int]) -> tuple[list[int], list[float]]: payload = { "model": self.model_name, "prompt": prompt_ids, "max_tokens": self.max_tokens, "temperature": self.temperature, "n": 1, "return_token_ids": True, "logprobs": 0, } while True: try: output = await self._post("/v1/completions", payload, self.request_timeout) break except (aiohttp.ServerDisconnectedError, aiohttp.ClientConnectionError, aiohttp.ClientResponseError): # vLLM drops connections or returns 503 during weight sync (/pause). Wait briefly and retry. logger.debug("Server unavailable (likely weight sync pause), retrying...") await asyncio.sleep(1.0) choice = output["choices"][0] completion_ids = choice["token_ids"] completion_logprobs = choice["logprobs"]["token_logprobs"] return completion_ids, completion_logprobs async def _score_group(self, group: RolloutGroup) -> list[RolloutSample]: kwargs = dict( completions=group.completions, prompt=group.prompt, prompts=[group.prompt] * len(group.completions), completion_ids=group.completions_ids, **group.reward_kwargs, ) all_rewards = await asyncio.gather( *[ reward_func(**kwargs) if inspect.iscoroutinefunction(reward_func) else asyncio.to_thread(reward_func, **kwargs) for reward_func in self.reward_funcs ] ) # Sum rewards across all reward functions. Reward functions may return None for individual # samples (e.g. accuracy_reward when the gold solution is unparseable). Convert None → nan # and use nansum so that a None from one function doesn't affect the others, matching TRL. all_rewards = [[r if r is not None else float("nan") for r in row] for row in all_rewards] rewards = np.nansum(np.array(all_rewards, dtype=float), axis=0) advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-8) reward_mean = float(rewards.mean()) reward_std = float(rewards.std()) logger.info(f"Rollout metrics: reward_mean={reward_mean:.4f}, reward_std={reward_std:.4f}") # tools/call_frequency: mean calls per completion (matches TRL's total_calls / num_completions) # tools/failure_frequency: per-completion failure rate; averaged across samples in compute_loss # (TRL uses total_failures / total_calls, ours weights equally per completion — close enough) total_calls = sum(group.tool_call_counts) tool_metrics = ( [ { "tools/call_frequency": float(n_calls), "tools/failure_frequency": (n_failures / n_calls) if n_calls > 0 else 0.0, } for n_calls, n_failures in zip(group.tool_call_counts, group.tool_failure_counts, strict=True) ] if total_calls > 0 else [{}] * len(group.completions) ) per_func_rewards = np.array(all_rewards, dtype=float) # shape (num_funcs, num_completions) return [ RolloutSample( prompt=group.prompt, completion=completion, input_ids=group.prompt_ids + completion_ids, completion_mask=[0] * len(group.prompt_ids) + tool_mask, old_log_probs=[0.0] * len(group.prompt_ids) + logprobs, advantage=advantage, model_version=group.model_version, metrics={ "reward": float(reward), "reward_std": reward_std, **{ f"rewards/{name}": float(func_reward) for name, func_reward in zip(self.reward_func_names, per_func_rewards[:, i], strict=True) }, **tm, }, ) for i, (completion, completion_ids, logprobs, tool_mask, advantage, reward, tm) in enumerate( zip( group.completions, group.completions_ids, group.completions_logprobs, group.tool_mask, advantages, rewards, tool_metrics, strict=True, ) ) ] async def _post(self, path: str, payload: dict, timeout: float, max_retries: int = 3) -> dict: client_timeout = aiohttp.ClientTimeout(total=timeout) for attempt in range(max_retries): try: async with self.session.post( f"{self.vllm_server_url}{path}", json=payload, timeout=client_timeout ) as response: response.raise_for_status() content = await response.json() return content if content else {} except (TimeoutError, asyncio.TimeoutError): if attempt < max_retries - 1: logger.warning(f"POST {path} timed out (attempt {attempt + 1}/{max_retries}), retrying...") await asyncio.sleep(1) else: raise ================================================ FILE: trl/experimental/bco/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .bco_config import BCOConfig from .bco_trainer import BCOTrainer ================================================ FILE: trl/experimental/bco/bco_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from ...trainer.base_config import _BaseConfig @dataclass class BCOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.bco.BCOTrainer`]. This class includes only the parameters that are specific to BCO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the sequences (prompt + completion) in the batch. This argument is required if you want to use the default data collator. max_completion_length (`int`, *optional*): Maximum length of the completion. This argument is required if you want to use the default data collator and your model is an encoder-decoder. beta (`float`, *optional*, defaults to `0.1`): Parameter controlling the deviation from the reference model. Higher β means less deviation from the reference model. truncation_mode (`str`, *optional*, defaults to `"keep_end"`): Truncation mode to use when the prompt is too long. Possible values are `"keep_end"` or `"keep_start"`. This argument is required if you want to use the default data collator. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model and reference model. generate_during_eval (`bool`, *optional*, defaults to `False`): If `True`, generates and logs completions from both the model and the reference model to W&B or Comet during evaluation. is_encoder_decoder (`bool`, *optional*): When using the `model_init` argument (callable) to instantiate the model instead of the `model` argument, you need to specify if the model returned by the callable is an encoder-decoder model. precompute_ref_log_probs (`bool`, *optional*, defaults to `False`): Whether to precompute reference model log probabilities for training and evaluation datasets. This is useful when training without the reference model to reduce the total GPU memory needed. model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model and reference model from strings. dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. prompt_sample_size (`int`, *optional*, defaults to `1024`): Number of prompts that are fed to density ratio classifier. min_density_ratio (`float`, *optional*, defaults to `0.5`): Minimum value of the density ratio. The estimated density ratio is clamped to this value. max_density_ratio (`float`, *optional*, defaults to `10.0`): Maximum value of the density ratio. The estimated density ratio is clamped to this value. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `5e-7` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=5e-7, metadata={"help": "The initial learning rate for AdamW."}, ) max_length: int | None = field( default=1024, metadata={ "help": "Maximum length of the sequences (prompt + completion) in the batch. " "This argument is required if you want to use the default data collator." }, ) max_completion_length: int | None = field( default=None, metadata={ "help": "Maximum length of the completion. This argument is required if you want to use the " "default data collator and your model is an encoder-decoder." }, ) beta: float = field( default=0.1, metadata={ "help": "Parameter controlling the deviation from the reference model. " "Higher β means less deviation from the reference model." }, ) truncation_mode: str = field( default="keep_end", metadata={ "help": "Truncation mode to use when the prompt is too long. Possible values are " "`keep_end` or `keep_start`. This argument is required if you want to use the " "default data collator." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model and reference model."}, ) generate_during_eval: bool = field( default=False, metadata={ "help": "If `True`, generates and logs completions from both the model and the reference model " "to W&B during evaluation." }, ) is_encoder_decoder: bool | None = field( default=None, metadata={ "help": "When using the `model_init` argument (callable) to instantiate the model instead of the " "`model` argument, you need to specify if the model returned by the callable is an " "encoder-decoder model." }, ) precompute_ref_log_probs: bool = field( default=False, metadata={ "help": "Whether to precompute reference model log probabilities for training and evaluation datasets. " "This is useful when training without the reference model to reduce the total GPU memory " "needed." }, ) model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the " "model from a string." }, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) prompt_sample_size: int = field( default=1024, metadata={"help": "Number of prompts that are fed to density ratio classifier."}, ) min_density_ratio: float = field( default=0.5, metadata={"help": "Minimum value of the density ratio. The estimated density ratio is clamped to this value."}, ) max_density_ratio: float = field( default=10.0, metadata={"help": "Maximum value of the density ratio. The estimated density ratio is clamped to this value."}, ) ================================================ FILE: trl/experimental/bco/bco_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import dataclasses import inspect import json import os import random import textwrap from collections import defaultdict from collections.abc import Callable from contextlib import contextmanager, nullcontext from dataclasses import dataclass from operator import itemgetter from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, Optional import numpy as np import pandas as pd import torch import torch.nn as nn import torch.nn.functional as F import transformers from accelerate import Accelerator, PartialState, logging from accelerate.utils import tqdm from datasets import Dataset from packaging.version import Version from torch import autocast from torch.utils.data import DataLoader, SequentialSampler from transformers import ( AutoModelForCausalLM, BaseImageProcessor, DataCollator, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, TrainingArguments, is_comet_available, is_sklearn_available, is_wandb_available, ) from transformers.trainer_utils import EvalLoopOutput, has_length from transformers.utils import is_peft_available from ...data_utils import maybe_apply_chat_template, maybe_extract_prompt, maybe_unpair_preference_dataset from ...import_utils import is_joblib_available from ...models.utils import prepare_deepspeed from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import disable_dropout_in_model, log_table_to_comet_experiment, selective_log_softmax from ..utils import DPODataCollatorWithPadding, create_reference_model, pad_to_length, peft_module_casting_to_bf16 from .bco_config import BCOConfig if is_peft_available(): from peft import PeftModel, get_peft_model, prepare_model_for_kbit_training if is_wandb_available(): import wandb if is_sklearn_available(): from sklearn.linear_model import LogisticRegression if is_joblib_available(): import joblib if TYPE_CHECKING: from transformers import PreTrainedTokenizer logger = logging.get_logger(__name__) RUNNING_NAME = "running.json" CLF_NAME = "clf.pkl" @torch.no_grad() def get_global_statistics( accelerator, xs: torch.Tensor, mask=None, device="cpu" ) -> tuple[torch.Tensor, torch.Tensor, int]: """ Computes element-wise mean and variance of the tensor across processes. Reference: https://github.com/OpenLMLab/MOSS-RLHF/blob/40b91eb2f2b71b16919addede0341d2bef70825d/utils.py#L57C1-L73C75 """ xs = xs.to(accelerator.device) sum_and_count = torch.tensor([xs.sum(), (xs.numel() if mask is None else mask.sum())], device=xs.device) sum_and_count = accelerator.reduce(sum_and_count) global_sum, count = sum_and_count global_mean = global_sum / count sum_var = torch.sum(((xs - global_mean) ** 2).mul(1 if mask is None else mask)) sum_var = accelerator.reduce(sum_var) global_var = sum_var / count return global_mean.to(device), global_var.to(device), count.item() @dataclass class RunningMoments: """ Calculates the running mean and standard deviation of a data stream. Reference: https://github.com/OpenLMLab/MOSS-RLHF/blob/40b91eb2f2b71b16919addede0341d2bef70825d/utils.py#L75 """ accelerator: Accelerator mean: float = 0 std: float = 1 var: float = 1 count: float = 1e-24 @torch.no_grad() def update(self, xs: torch.Tensor) -> tuple[float, float]: """ Updates running moments from batch's moments computed across ranks """ if self.accelerator.use_distributed: xs_mean, xs_var, xs_count = get_global_statistics(self.accelerator, xs) else: xs_count = xs.numel() xs_var, xs_mean = torch.var_mean(xs, unbiased=False) xs_mean, xs_var = xs_mean.float(), xs_var.float() delta = xs_mean - self.mean tot_count = self.count + xs_count new_sum = xs_var * xs_count # correct old_sum deviation accounting for the new mean old_sum = self.var * self.count + delta**2 * self.count * xs_count / tot_count tot_sum = old_sum + new_sum self.mean += (delta * xs_count / tot_count).item() new_var = tot_sum / tot_count self.std = (new_var * tot_count / (tot_count - 1)).float().sqrt().item() self.var = new_var.item() self.count = tot_count return xs_mean.item(), (xs_var * xs_count / (xs_count - 1)).float().sqrt().item() def save_to_json(self, json_path: str): """Save the content of this instance in JSON format inside `json_path`.""" # save everything except accelerator if self.accelerator.is_main_process: save_dict = dataclasses.asdict(self, dict_factory=lambda x: {k: v for (k, v) in x if k != "accelerator"}) json_string = json.dumps(save_dict, indent=2, sort_keys=True) + "\n" with open(json_path, "w", encoding="utf-8") as f: f.write(json_string) @classmethod def load_from_json(cls, accelerator: Accelerator, json_path: str): """Create an instance from the content of `json_path`.""" # load everything except accelerator with open(json_path, encoding="utf-8") as f: text = f.read() return cls(accelerator=accelerator, **json.loads(text)) def _tokenize( batch: dict[str, list[Any]], tokenizer: "PreTrainedTokenizer", embedding_tokenizer: Optional["PreTrainedTokenizer"] = None, ) -> dict[str, list[Any]]: """Tokenize a batch from a BCO specific dataset.""" prompt_tokenized = tokenizer(batch["prompt"], add_special_tokens=False) prompt_input_ids = prompt_tokenized["input_ids"] prompt_attention_mask = prompt_tokenized["attention_mask"] prompt_and_completion = [ prompt + completion for prompt, completion in zip(batch["prompt"], batch["completion"], strict=True) ] full_tokenized = tokenizer(prompt_and_completion, add_special_tokens=False) full_input_ids = full_tokenized["input_ids"] full_attention_mask = full_tokenized["attention_mask"] answer_input_ids = [f[len(p) :] for f, p in zip(full_input_ids, prompt_input_ids, strict=True)] answer_attention_mask = [f[len(p) :] for f, p in zip(full_attention_mask, prompt_attention_mask, strict=True)] # Concat tokens to form `enc(a) + enc(a + b)[len(enc(a)):]` full_concat_input_ids = [np.concatenate([p, a]) for p, a in zip(prompt_input_ids, answer_input_ids, strict=True)] # Prepare input tokens for token by token comparison full_input_ids = [np.array(f) for f in full_input_ids] for full, concat in zip(full_input_ids, full_concat_input_ids, strict=True): if len(full) != len(concat): raise ValueError( "The elements in 'full_input_ids' and 'full_concat_input_ids' must have the same pairwise length." ) # On some tokenizers, like Llama-2 tokenizer, there are occasions where tokens # can be merged together when tokenizing prompt+answer. This could result # on the last token from the prompt being different when tokenized on its own # vs when done as prompt+answer. response_token_ids_start_idx = [len(p) for p in prompt_input_ids] # If tokenized prompt is different than both prompt+answer, then it means the # last token has changed due to merging. for idx, (p, f, r) in enumerate(zip(prompt_input_ids, full_input_ids, response_token_ids_start_idx, strict=True)): if not np.array_equal(p, f[:r]): response_token_ids_start_idx[idx] -= 1 prompt_input_ids = [f[:r] for f, r in zip(full_input_ids, response_token_ids_start_idx, strict=True)] prompt_attention_mask = [f[:r] for f, r in zip(full_attention_mask, response_token_ids_start_idx, strict=True)] for p, m in zip(prompt_input_ids, prompt_attention_mask, strict=True): if len(p) != len(m): raise ValueError("Prompt input ids and attention mask should have the same length.") answer_input_ids = [f[r:] for f, r in zip(full_input_ids, response_token_ids_start_idx, strict=True)] answer_attention_mask = [f[r:] for f, r in zip(full_attention_mask, response_token_ids_start_idx, strict=True)] output = dict( prompt_input_ids=prompt_input_ids, prompt_attention_mask=prompt_attention_mask, answer_input_ids=answer_input_ids, answer_attention_mask=answer_attention_mask, ) if embedding_tokenizer is not None: embedding_tokenized = embedding_tokenizer(batch["prompt"], add_special_tokens=False) output.update( { "embedding_input_ids": embedding_tokenized["input_ids"], "embedding_attention_mask": embedding_tokenized["attention_mask"], } ) return output def _process_tokens(example: dict[str, Any], model: "PreTrainedModel" = None, **kwargs) -> dict: """Process tokens of a BCO specific dataset. At this stage, we don't convert to PyTorch tensors yet; we just handle the truncation in case the prompt + completion responses is/are too long. First we truncate the prompt; if we're still too long, we truncate the completion. We also create the labels for the completion responses, which are of length equal to the sum of the length of the prompt and the completion response, with `-100` for the prompt tokens. """ prompt = example["prompt"] completion = example["completion"] batch = { f"{kwargs['prefix']}prompt": prompt, f"{kwargs['prefix']}completion": completion, f"{kwargs['prefix']}label": example["label"], } if not kwargs["is_encoder_decoder"]: # Check issues below for more details # 1. https://github.com/huggingface/trl/issues/907 # 2. https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 # 3. https://github.com/LianjiaTech/BELLE/issues/337 if not isinstance(prompt, str): raise ValueError(f"prompt should be an str but got {type(prompt)}") if not isinstance(completion, str): raise ValueError(f"completion should be an str but got {type(completion)}") # keys of format prompt_* refers to just the prompt and answer_* refers to just the answer all_tokens = { "prompt_input_ids": example["prompt_input_ids"], "prompt_attention_mask": example["prompt_attention_mask"], "answer_input_ids": example["answer_input_ids"], "answer_attention_mask": example["answer_attention_mask"], } # calculate max length by checking if BOS/EOS is already there max_length = kwargs["max_length"] bos_token_id = kwargs["tokenizer"].bos_token_id eos_token_id = kwargs["tokenizer"].eos_token_id if bos_token_id != all_tokens["prompt_input_ids"][0]: max_length -= 1 if eos_token_id != all_tokens["answer_input_ids"][-1]: max_length -= 1 # if combined sequence is too long (> max_length - 1 for BOS token - 1 for EOS), truncate the response if len(all_tokens["prompt_input_ids"]) + len(all_tokens["answer_input_ids"]) > max_length: for k in ["answer_input_ids", "answer_attention_mask"]: all_tokens[k] = all_tokens[k][: max_length - len(all_tokens["prompt_input_ids"])] # all input_ids and attention mask as is. We then check if we need to add BOS/EOS tokens batch[f"{kwargs['prefix']}prompt_input_ids"] = all_tokens["prompt_input_ids"] batch[f"{kwargs['prefix']}prompt_attention_mask"] = all_tokens["prompt_attention_mask"] batch[f"{kwargs['prefix']}completion_input_ids"] = ( all_tokens["prompt_input_ids"] + all_tokens["answer_input_ids"] ) batch[f"{kwargs['prefix']}completion_attention_mask"] = ( all_tokens["prompt_attention_mask"] + all_tokens["answer_attention_mask"] ) # add BOS, which affects both prompt and the full completion if bos_token_id is not None: if len(all_tokens["prompt_input_ids"]) == 0 or bos_token_id != all_tokens["prompt_input_ids"][0]: batch[f"{kwargs['prefix']}prompt_input_ids"] = [bos_token_id] + batch[ f"{kwargs['prefix']}prompt_input_ids" ] batch[f"{kwargs['prefix']}prompt_attention_mask"] = [1] + batch[ f"{kwargs['prefix']}prompt_attention_mask" ] batch[f"{kwargs['prefix']}completion_input_ids"] = [bos_token_id] + batch[ f"{kwargs['prefix']}completion_input_ids" ] batch[f"{kwargs['prefix']}completion_attention_mask"] = [1] + batch[ f"{kwargs['prefix']}completion_attention_mask" ] # add EOS, which affects only the full completion if len(all_tokens["answer_input_ids"]) == 0 or eos_token_id != all_tokens["answer_input_ids"][-1]: batch[f"{kwargs['prefix']}completion_input_ids"] = batch[f"{kwargs['prefix']}completion_input_ids"] + [ eos_token_id ] batch[f"{kwargs['prefix']}completion_attention_mask"] = batch[ f"{kwargs['prefix']}completion_attention_mask" ] + [1] batch[f"{kwargs['prefix']}completion_labels"] = batch[f"{kwargs['prefix']}completion_input_ids"][:] batch[f"{kwargs['prefix']}completion_labels"][: len(batch[f"{kwargs['prefix']}prompt_input_ids"])] = [ -100 ] * len(batch[f"{kwargs['prefix']}prompt_input_ids"]) else: completion_tokens = kwargs["tokenizer"]( completion, truncation=True, max_length=kwargs["max_completion_length"], add_special_tokens=True ) prompt_tokens = kwargs["tokenizer"](prompt, add_special_tokens=True) batch[f"{kwargs['prefix']}prompt_input_ids"] = prompt_tokens["input_ids"] batch[f"{kwargs['prefix']}prompt_attention_mask"] = prompt_tokens["attention_mask"] batch[f"{kwargs['prefix']}completion_labels"] = completion_tokens["input_ids"] batch[f"{kwargs['prefix']}completion_attention_mask"] = completion_tokens["attention_mask"] if model is not None and hasattr(model, "prepare_decoder_input_ids_from_labels"): batch[f"{kwargs['prefix']}completion_decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels( labels=torch.tensor(batch["completion_labels"]) ) return batch class BCOTrainer(_BaseTrainer): r""" Initialize BCOTrainer from [BCO](https://huggingface.co/papers/2404.04656) paper. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an [`~transformers.AutoModelForSequenceClassification`]. ref_model ([`~transformers.PreTrainedModel`]): Hugging Face transformer model with a casual language modelling head. Used for implicit reward computation and loss. If no reference model is provided, the trainer will create a reference model with the same architecture as the model to be optimized. args ([`experimental.bco.BCOConfig`]): The arguments to use for training. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. data_collator ([`~transformers.DataCollator`], *optional*): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. model_init (`Callable[[], transformers.PreTrainedModel]`): The model initializer to use for training. If None is specified, the default model initializer will be used. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. peft_config (`dict`, defaults to `None`): The PEFT configuration to use for training. If you pass a PEFT configuration, the model will be wrapped in a PEFT model. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. model_adapter_name (`str`, defaults to `None`): Name of the train target PEFT adapter, when using LoRA with multiple adapters. ref_adapter_name (`str`, defaults to `None`): Name of the reference PEFT adapter, when using LoRA with multiple adapters. """ _tag_names = ["trl", "bco"] _name = "BCO" _paper = { "title": "Binary Classifier Optimization for Large Language Model Alignment", "id": "2404.04656", # docstyle-ignore "citation": textwrap.dedent("""\ @article{jung2024binary, title = {{Binary Classifier Optimization for Large Language Model Alignment}}, author = {Seungjae Jung and Gunsoo Han and Daniel Wontae Nam and Kyoung{-}Woon On}, year = 2024, eprint = {arXiv:2404.04656} }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str = None, ref_model: PreTrainedModel | nn.Module | str | None = None, args: BCOConfig = None, train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, data_collator: DataCollator | None = None, model_init: Callable[[], PreTrainedModel] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalLoopOutput], dict] | None = None, model_adapter_name: str | None = None, ref_adapter_name: str | None = None, embedding_func: Callable | None = None, embedding_tokenizer: PreTrainedTokenizerBase | None = None, ): if embedding_func is not None and not (is_sklearn_available() and is_joblib_available()): raise ImportError( "BCOTrainer with UDM requires the scikit-learn and joblib libraries. Please install it with `pip install scikit-learn joblib`." ) if type(args) is TrainingArguments: raise ValueError("Please use `BCOConfig` instead `TrainingArguments`.") if train_dataset is None: raise ValueError("`train_dataset` is required") if not isinstance(model, str) and model is not None and ref_model is model: raise ValueError( "`model` and `ref_model` cannot be the same object. If you want `ref_model` to be the " "same as `model`, you must mass a copy of it, or `None` if you use peft." ) if args.model_init_kwargs is None: model_init_kwargs = {} elif not isinstance(model, str): raise ValueError("You passed model_kwargs to the BCOTrainer. But your model is already instantiated.") else: model_init_kwargs = args.model_init_kwargs dtype = model_init_kwargs.get("dtype", "auto") if dtype is not None: # Convert to `torch.dtype` if an str is passed if isinstance(dtype, str) and dtype != "auto": dtype = getattr(torch, dtype) if dtype != "auto" and not isinstance(dtype, torch.dtype): raise ValueError( f"Invalid `dtype` passed to the BCOConfig. Expected a string with either `torch.dtype` or 'auto', but got {dtype}." ) model_init_kwargs["dtype"] = dtype model_init_kwargs["device_map"] = model_init_kwargs.get("device_map", "auto") if isinstance(model, str): model = AutoModelForCausalLM.from_pretrained(model, **model_init_kwargs) if isinstance(ref_model, str): ref_model = AutoModelForCausalLM.from_pretrained(ref_model, **model_init_kwargs) # Initialize this variable to False. This helps tracking the case when `peft_module_casting_to_bf16` # has been called in order to properly call autocast if needed. self._peft_has_been_casted_to_bf16 = False if not is_peft_available() and peft_config is not None: raise ValueError( "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it with `pip install peft` to use the PEFT models" ) elif is_peft_available() and peft_config is not None: if isinstance(model, PeftModel): raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first " "merge and unload the existing adapter, save the resulting base model, and then pass that base " "model along with the new `peft_config` to the trainer." ) if getattr(model, "is_loaded_in_8bit", False) or getattr(model, "is_loaded_in_4bit", False): _support_gc_kwargs = hasattr( args, "gradient_checkpointing_kwargs" ) and "gradient_checkpointing_kwargs" in list( inspect.signature(prepare_model_for_kbit_training).parameters ) prepare_model_kwargs = {"use_gradient_checkpointing": args.gradient_checkpointing} if _support_gc_kwargs: prepare_model_kwargs["gradient_checkpointing_kwargs"] = args.gradient_checkpointing_kwargs model = prepare_model_for_kbit_training(model, **prepare_model_kwargs) elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) # get peft model with the given config model = get_peft_model(model, peft_config) if args.bf16 and getattr(model, "is_loaded_in_4bit", False): peft_module_casting_to_bf16(model) # If args.bf16 we need to explicitly call `generate` with torch amp autocast context manager self._peft_has_been_casted_to_bf16 = True # For models that use gradient_checkpointing, we need to attach a hook that enables input # to explicitly have `requires_grad=True`, otherwise training will either silently # fail or completely fail. elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) if args.generate_during_eval and not (is_wandb_available() or is_comet_available()): raise ValueError( "`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve." ) if model is not None: self.is_encoder_decoder = model.config.is_encoder_decoder elif args.is_encoder_decoder is None: raise ValueError("When no model is provided, you need to pass the parameter is_encoder_decoder.") else: self.is_encoder_decoder = args.is_encoder_decoder self.is_peft_model = is_peft_available() and isinstance(model, PeftModel) self.model_adapter_name = model_adapter_name self.ref_adapter_name = ref_adapter_name if ref_model: self.ref_model = ref_model elif self.is_peft_model or args.precompute_ref_log_probs: # The `model` with adapters turned off will be used as the reference model self.ref_model = None else: self.ref_model = create_reference_model(model) if processing_class is None: raise ValueError( "max_length or a processing_class must be specified when using the default DPODataCollatorWithPadding" ) if args.max_length is None: logger.warning( "When using DPODataCollatorWithPadding, you should set `max_length` in the `BCOConfig`. " "It will be set to `512` by default, but you should do it yourself in the future.", ) max_length = 512 if args.max_length is not None: max_length = args.max_length max_completion_length = None if args.max_completion_length is None and self.is_encoder_decoder: logger.warning( "When using DPODataCollatorWithPadding with an encoder decoder architecture, you should set `max_completion_length` in the BCOTrainer's init" " it will be set to `128` by default, but you should do it yourself in the future.", ) max_completion_length = 128 if args.max_completion_length is not None and self.is_encoder_decoder: max_completion_length = args.max_completion_length if data_collator is None: data_collator = DPODataCollatorWithPadding( pad_token_id=processing_class.pad_token_id, is_encoder_decoder=self.is_encoder_decoder, ) if args.remove_unused_columns: args.remove_unused_columns = False # warn users logger.warning( "When using DPODataCollatorWithPadding, you should set `remove_unused_columns=False` in your BCOConfig" " we have set it for you, but you should do it yourself in the future.", ) self.use_dpo_data_collator = True else: self.use_dpo_data_collator = False # Disable dropout in the model and reference model if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) self.max_length = max_length self.generate_during_eval = args.generate_during_eval self.truncation_mode = args.truncation_mode self.max_completion_length = max_completion_length self.precompute_ref_log_probs = args.precompute_ref_log_probs # Since ref_logs are precomputed on the first call to get_train/eval_dataloader # keep track of first called to avoid computation of future calls self._precomputed_train_ref_log_probs = False self._precomputed_eval_ref_log_probs = False # metric self._stored_metrics = defaultdict(lambda: defaultdict(list)) # BCO parameter self.beta = args.beta self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) self.aux_loss_coef = getattr(model.config, "router_aux_loss_coef", 0.0) if self.aux_loss_enabled and self.aux_loss_coef == 0.0: logger.warning( "You set `output_router_logits` to `True` in the model config, but `router_aux_loss_coef` is set to " "`0.0`, meaning the auxiliary loss will not be used. Either set `router_aux_loss_coef` to a value " "greater than `0.0`, or set `output_router_logits` to `False` if you don't want to use the auxiliary " "loss.", ) # Underlying Distribution Matching argument self.embedding_func = embedding_func self.embedding_tokenizer = embedding_tokenizer with PartialState().main_process_first(): # Extract the prompt if needed train_dataset = train_dataset.map( maybe_extract_prompt, num_proc=args.dataset_num_proc, desc="Extracting prompt from train dataset" ) # Unpair the dataset if needed train_dataset = maybe_unpair_preference_dataset( train_dataset, args.dataset_num_proc, desc="Unpairing train dataset" ) # Apply the chat template if needed train_dataset = train_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc ) if eval_dataset is not None: # Extract the prompt if needed eval_dataset = eval_dataset.map( maybe_extract_prompt, num_proc=args.dataset_num_proc, desc="Extracting prompt from eval dataset" ) # Unpair the dataset if needed eval_dataset = maybe_unpair_preference_dataset( eval_dataset, args.dataset_num_proc, desc="Unpairing eval dataset" ) eval_dataset = eval_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc, ) # Tokenize and prepare the training datasets train_dataset = train_dataset.map( _tokenize, batched=True, fn_kwargs={"tokenizer": processing_class, "embedding_tokenizer": self.embedding_tokenizer}, num_proc=args.dataset_num_proc, desc="Tokenizing train dataset", ) # Prepare the datasets fn_kwargs = { "prefix": "", "is_encoder_decoder": self.is_encoder_decoder, "tokenizer": processing_class, "max_length": self.max_length, "truncation_mode": self.truncation_mode, "max_completion_length": self.max_completion_length, } train_dataset = train_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, desc="Processing tokenized train dataset", ) if eval_dataset is not None: # Tokenize eval_dataset = eval_dataset.map( _tokenize, fn_kwargs={"tokenizer": processing_class, "embedding_tokenizer": self.embedding_tokenizer}, batched=True, num_proc=args.dataset_num_proc, desc="Tokenizing eval dataset", ) # Process fn_kwargs = { "prefix": "", "is_encoder_decoder": self.is_encoder_decoder, "tokenizer": processing_class, "max_length": self.max_length, "truncation_mode": self.truncation_mode, "max_completion_length": self.max_completion_length, } eval_dataset = eval_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, desc="Processing tokenized eval dataset", ) desirable = train_dataset.filter( lambda x: x["label"], num_proc=args.dataset_num_proc, desc="Filtering desirable examples" ) undesirable = train_dataset.filter( lambda x: not x["label"], num_proc=args.dataset_num_proc, desc="Filtering undesirable examples" ) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, model_init=model_init, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) if not hasattr(self, "accelerator"): raise AttributeError( "Your `Trainer` does not have an `accelerator` object. Consider upgrading `transformers`." ) # Deepspeed Zero-3 does not support precompute_ref_log_probs if self.is_deepspeed_enabled: if self.accelerator.state.deepspeed_plugin.zero_stage == 3 and self.precompute_ref_log_probs: raise ValueError( "You cannot use `precompute_ref_log_probs=True` with Deepspeed ZeRO-3. Please set `precompute_ref_log_probs=False`." ) if self.ref_model is None: if not (self.is_peft_model or self.precompute_ref_log_probs): raise ValueError( "No reference model and model is not a Peft model. Try setting `precompute_ref_log_probs=True`" ) else: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) self.running = RunningMoments(accelerator=self.accelerator) if self.embedding_func is None or args.resume_from_checkpoint: return chosen_embeddings = self._get_sample_prompt_embeddings(desirable, sample_size=self.args.prompt_sample_size) rejected_embeddings = self._get_sample_prompt_embeddings(undesirable, sample_size=self.args.prompt_sample_size) embeddings = torch.cat((chosen_embeddings, rejected_embeddings), dim=0) labels = torch.cat( (torch.ones_like(chosen_embeddings[:, 0]), torch.zeros_like(rejected_embeddings[:, 0])), dim=0 ) self.clf = LogisticRegression(class_weight="balanced").fit( embeddings.cpu().float().numpy(), labels.cpu().numpy() ) chosen_mean = self.clf.score( chosen_embeddings.cpu().float().numpy(), torch.ones_like(chosen_embeddings[:, 0]).cpu().numpy() ) rejected_mean = self.clf.score( rejected_embeddings.cpu().float().numpy(), torch.zeros_like(rejected_embeddings[:, 0]).cpu().numpy() ) logger.info(f"UDM classifier training scores: chosen: {chosen_mean}, rejected: {rejected_mean}") @property def match_underlying_distribution(self): return self.embedding_func is not None and self.embedding_tokenizer is not None def _get_chosen_prob(self, prompt_embeddings: torch.FloatTensor) -> torch.FloatTensor: """ Calculates the probability if the given prompt embedding is from desirable dataset. This function calculates the probability in the process and ensemble across processes. """ dtype = prompt_embeddings.dtype device = prompt_embeddings.device rank = self.accelerator.process_index padded_prompt_embeddings = self.accelerator.pad_across_processes( prompt_embeddings, pad_index=self.embedding_tokenizer.pad_token_id ) sample_size = padded_prompt_embeddings.shape[0] nonzero = padded_prompt_embeddings.mean(dim=1) != self.embedding_tokenizer.pad_token_id prompt_embeddings = self.accelerator.gather(padded_prompt_embeddings) # cannot predict for all empty values if prompt_embeddings.shape[0] == 0: return torch.tensor([], device=device, dtype=dtype) prob = self.clf.predict_proba(prompt_embeddings.cpu().float().numpy())[:, 1] prob = torch.as_tensor(prob, dtype=dtype, device=device) prob = self.accelerator.reduce(prob, reduction="mean") prob = prob[sample_size * rank : sample_size * (rank + 1)] prob = prob[nonzero] return prob def _vectorize_prompt(self, input_ids: torch.LongTensor, attention_mask: torch.LongTensor) -> torch.FloatTensor: """ Replaces processing_class.pad_token_id to embedding_tokenizer.pad_token_id and applies self.embedding_func """ input_ids = torch.where( input_ids == self.processing_class.pad_token_id, self.embedding_tokenizer.pad_token_id, input_ids, ) with torch.no_grad(): embeddings = self.embedding_func( input_ids=input_ids, attention_mask=attention_mask, ) return embeddings def _get_prompt_embeddings( self, batch: dict[str, list | torch.LongTensor] ) -> tuple[torch.FloatTensor, torch.FloatTensor]: """Extract embeddings from frozen embedding model""" if not self.match_underlying_distribution: return None, None embeddings = self._vectorize_prompt( input_ids=batch["embedding_input_ids"], attention_mask=batch["embedding_attention_mask"], ) labels = torch.tensor(batch["label"], dtype=torch.bool, device=embeddings.device) chosen_idx = torch.where(labels)[0] rejected_idx = torch.where(~labels)[0] chosen_embeddings = embeddings[chosen_idx, ...] rejected_embeddings = embeddings[rejected_idx, ...] return (chosen_embeddings, rejected_embeddings) def _get_sample_prompt_embeddings(self, dataset: Dataset, sample_size: int = 512) -> torch.FloatTensor: """ Sample instances from dataset and get prompt embeddings. Used for density ratio classifier training. """ n_samples = min(len(dataset), sample_size) rand_indices = np.random.choice(len(dataset), size=(n_samples,)) embedding_dataset = dataset.select(rand_indices) dataloader_params = { "batch_size": self.args.per_device_train_batch_size, "collate_fn": self.data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "shuffle": False, } # prepare dataloader data_loader = self.accelerator.prepare(DataLoader(embedding_dataset, **dataloader_params)) with torch.no_grad(): all_embeddings = torch.empty(0) for padded_batch in tqdm(iterable=data_loader, desc="Building sample prompt embeddings"): embeddings = self._vectorize_prompt( input_ids=padded_batch["embedding_input_ids"], attention_mask=padded_batch["embedding_attention_mask"], ) embeddings = self.accelerator.gather_for_metrics(embeddings) all_embeddings = torch.cat((all_embeddings, embeddings.cpu())) return all_embeddings def _save_optimizer_and_scheduler(self, output_dir): output_dir = output_dir if output_dir is not None else self.args.output_dir super()._save_optimizer_and_scheduler(output_dir) if self.accelerator.is_main_process: # When saving optimizer and scheduler to checkpoint, save also the running delta object. self.running.save_to_json(os.path.join(output_dir, RUNNING_NAME)) if self.match_underlying_distribution: joblib.dump(self.clf, os.path.join(output_dir, CLF_NAME), compress=True) def _load_optimizer_and_scheduler(self, checkpoint): if checkpoint is None: logger.warning_once(f"Missing Checkpoint {checkpoint}") return super()._load_optimizer_and_scheduler(checkpoint) # when loading optimizer and scheduler from checkpoint, also load the running delta object. running_file = os.path.join(checkpoint, RUNNING_NAME) if os.path.isfile(running_file): self.running = RunningMoments.load_from_json(self.accelerator, running_file) if self.match_underlying_distribution: clf_file = os.path.join(checkpoint, CLF_NAME) if os.path.isfile(clf_file): self.clf = joblib.load(clf_file) @contextmanager def null_ref_context(self): """Context manager for handling null reference model (that is, peft adapter manipulation).""" with ( self.accelerator.unwrap_model(self.model).disable_adapter() if self.is_peft_model and not self.ref_adapter_name else nullcontext() ): if self.ref_adapter_name: self.model.set_adapter(self.ref_adapter_name) yield if self.ref_adapter_name: self.model.set_adapter(self.model_adapter_name or "default") def get_train_dataloader(self) -> DataLoader: """ Returns the training [`~torch.utils.data.DataLoader`]. Subclass of transformers.src.transformers.trainer.get_train_dataloader to precompute `ref_log_probs`. """ if self.precompute_ref_log_probs and not self._precomputed_train_ref_log_probs: dataloader_params = { "batch_size": self.args.per_device_train_batch_size, "collate_fn": self.data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "shuffle": False, } # prepare dataloader data_loader = self.accelerator.prepare(DataLoader(self.train_dataset, **dataloader_params)) reference_completion_logps = [] for padded_batch in tqdm(iterable=data_loader, desc="Train dataset reference log probs"): reference_completion_logp = self.compute_reference_log_probs(padded_batch) reference_completion_logp = self.accelerator.gather_for_metrics(reference_completion_logp) reference_completion_logps.append(reference_completion_logp.cpu()) self.train_dataset = self.train_dataset.add_column( name="reference_logps", column=torch.cat(reference_completion_logps).float().numpy() ) self._precomputed_train_ref_log_probs = True return super().get_train_dataloader() def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader: """ Returns the evaluation [`~torch.utils.data.DataLoader`]. Subclass of transformers.src.transformers.trainer.get_eval_dataloader to precompute `ref_log_probs`. Args: eval_dataset (`torch.utils.data.Dataset`, *optional*): If provided, will override `self.eval_dataset`. If it is a [`~datasets.Dataset`], columns not accepted by the `model.forward()` method are automatically removed. It must implement `__len__`. """ if eval_dataset is None and self.eval_dataset is None: raise ValueError("Trainer: evaluation requires an eval_dataset.") eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset if self.precompute_ref_log_probs and not self._precomputed_eval_ref_log_probs: dataloader_params = { "batch_size": self.args.per_device_eval_batch_size, "collate_fn": self.data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "shuffle": False, } # prepare dataloader data_loader = self.accelerator.prepare(DataLoader(eval_dataset, **dataloader_params)) reference_completion_logps = [] for padded_batch in tqdm(iterable=data_loader, desc="Eval dataset reference log probs"): reference_completion_logp = self.compute_reference_log_probs(padded_batch) reference_completion_logp = self.accelerator.gather_for_metrics(reference_completion_logp) reference_completion_logps.append(reference_completion_logp.cpu()) eval_dataset = eval_dataset.add_column( name="reference_logps", column=torch.cat(reference_completion_logps).float().numpy() ) # Save calculated reference_chosen_logps and reference_rejected_logps to the eval_dataset for subsequent runs if self.eval_dataset is not None: self.eval_dataset = eval_dataset self._precomputed_eval_ref_log_probs = True return super().get_eval_dataloader(eval_dataset=eval_dataset) def compute_reference_log_probs(self, padded_batch: dict) -> dict: """Computes log probabilities of the reference model for a single padded batch of a BCO specific dataset.""" with torch.no_grad(): if self.ref_model is None: with self.null_ref_context(): if self.is_encoder_decoder: completion_logits = self.model( padded_batch["prompt_input_ids"], attention_mask=padded_batch["prompt_attention_mask"], decoder_input_ids=padded_batch.get("completion_decoder_input_ids"), labels=padded_batch["completion_labels"], ).logits else: completion_logits = self.model( padded_batch["completion_input_ids"], attention_mask=padded_batch["completion_attention_mask"], ).logits else: if self.is_encoder_decoder: completion_logits = self.ref_model( padded_batch["prompt_input_ids"], attention_mask=padded_batch["prompt_attention_mask"], decoder_input_ids=padded_batch.get("completion_decoder_input_ids"), labels=padded_batch["completion_labels"], ).logits else: completion_logits = self.ref_model( padded_batch["completion_input_ids"], attention_mask=padded_batch["completion_attention_mask"] ).logits completion_logps = self.get_batch_logps( completion_logits, padded_batch["completion_labels"], average_log_prob=False, is_encoder_decoder=self.is_encoder_decoder, ) return completion_logps @staticmethod def get_batch_logps( logits: torch.FloatTensor, labels: torch.LongTensor, average_log_prob: bool = False, is_encoder_decoder: bool = False, ) -> torch.FloatTensor: """Compute the log probabilities of the given labels under the given logits. Args: logits: Logits of the model (unnormalized). Shape: (batch_size, sequence_length, vocab_size) labels: Labels for which to compute the log probabilities. Label tokens with a value of `-100` are ignored. Shape: (batch_size, sequence_length) average_log_prob: If True, return the average log probability per (non-masked) token. Otherwise, return the sum of the log probabilities of the (non-masked) tokens. is_encoder_decoder: Whether the model is an encoder-decoder model. If True, the labels are not shifted, and the logits are assumed to already be aligned with the labels. If False, the labels are shifted to the right by one position, and the logits are assumed to be aligned with the shifted labels. Returns: A tensor of shape (batch_size,) containing the average/sum log probabilities of the given labels under the given logits. """ if logits.shape[:-1] != labels.shape: raise ValueError("Logits (batch and sequence length dim) and labels must have the same shape.") if not is_encoder_decoder: labels = labels[:, 1:].clone() logits = logits[:, :-1, :] else: # Fixes end-dec RuntimeError labels = labels.clone() loss_mask = labels != -100 # dummy token; we'll ignore the losses on these tokens later labels[labels == -100] = 0 per_token_logps = selective_log_softmax(logits, labels) if average_log_prob: return (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) else: return (per_token_logps * loss_mask).sum(-1) def forward( self, model: nn.Module, batch: dict[str, list | torch.LongTensor] ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: model_kwargs = ( { "labels": batch["completion_labels"], "decoder_input_ids": batch.get("completion_decoder_input_ids"), } if self.is_encoder_decoder else {} ) if self.aux_loss_enabled: model_kwargs["output_router_logits"] = True outputs = model( batch["completion_input_ids"], attention_mask=batch["completion_attention_mask"], **model_kwargs, ) completion_logits = outputs.logits completion_logps = self.get_batch_logps( completion_logits, batch["completion_labels"], average_log_prob=False, is_encoder_decoder=self.is_encoder_decoder, ) if completion_logps.shape[0] != len(batch["label"]): raise ValueError( "There is a mismatch between the number of examples in this batch and the number of " "examples for which an output sequence was predicted." ) chosen_idx = [i for i in range(completion_logps.shape[0]) if batch["label"][i] is True] rejected_idx = [i for i in range(completion_logps.shape[0]) if batch["label"][i] is False] chosen_logps = completion_logps[chosen_idx, ...] rejected_logps = completion_logps[rejected_idx, ...] chosen_logits = completion_logits[chosen_idx, ...] rejected_logits = completion_logits[rejected_idx, ...] if self.aux_loss_enabled: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, outputs.aux_loss) else: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits) def _get_udm_weight(self, rejected_embeddings: torch.FloatTensor) -> torch.FloatTensor: prob_desirable = self._get_chosen_prob(rejected_embeddings) min_ratio = self.args.min_density_ratio max_ratio = self.args.max_density_ratio weight = (prob_desirable / (1 - prob_desirable + 1e-8)).clamp(min=min_ratio, max=max_ratio) return weight def bco_loss( self, policy_chosen_logps: torch.FloatTensor, policy_rejected_logps: torch.FloatTensor, reference_chosen_logps: torch.FloatTensor, reference_rejected_logps: torch.FloatTensor, chosen_embeddings: torch.FloatTensor | None, rejected_embeddings: torch.FloatTensor | None, do_train: bool = True, ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Compute the BCO loss for a batch of policy and reference model log probabilities. Args: policy_chosen_logps: Log probabilities of the policy model for the chosen responses. Shape: (num(chosen) in batch_size,) policy_rejected_logps: Log probabilities of the policy model for the rejected responses. Shape: (num(rejected) in batch_size,) reference_chosen_logps: Log probabilities of the reference model for the chosen responses. Shape: (num(chosen) in batch_size,) reference_rejected_logps: Log probabilities of the reference model for the rejected responses. Shape: (num(rejected) in batch_size,) chosen_embeddings: embeddings of desirable prompts rejected_embeddings: embeddings of undesirable prompts do_train: whether to update the running delta value. Default is True. Returns: A tuple of four tensors: (losses, chosen_rewards, rejected_rewards, delta). The losses tensor contains the BCO loss for each example in the batch. The chosen_rewards and rejected_rewards tensors contain the rewards for the chosen and rejected responses, respectively. The delta value contains the moving average of all implicit rewards. """ chosen_logratios = policy_chosen_logps - reference_chosen_logps chosen_rewards = self.beta * chosen_logratios rejected_logratios = policy_rejected_logps - reference_rejected_logps rejected_rewards = self.beta * rejected_logratios if do_train: self.running.update(torch.cat((chosen_rewards, rejected_rewards), 0).detach()) delta = torch.as_tensor(self.running.mean, device=chosen_rewards.device) chosen_losses = -F.logsigmoid(chosen_rewards - delta) rejected_losses = -F.logsigmoid(-(rejected_rewards - delta)) if self.match_underlying_distribution: chosen_weight = torch.ones_like(chosen_losses) rejected_weight = self._get_udm_weight(rejected_embeddings) losses = torch.cat((chosen_weight * chosen_losses, rejected_weight * rejected_losses), dim=0) else: losses = torch.cat((chosen_losses, rejected_losses), dim=0) return losses, chosen_rewards, rejected_rewards, delta def get_batch_loss_metrics( self, model, batch: dict[str, list | torch.LongTensor], do_train: bool = True, ): """Compute the BCO loss and other metrics for the given batch of inputs for train or test.""" metrics = {} batch = {k: (v.to(self.accelerator.device) if isinstance(v, torch.Tensor) else v) for k, v in batch.items()} forward_output = self.forward(model, batch) ( policy_chosen_logps, policy_rejected_logps, policy_chosen_logits, policy_rejected_logits, ) = forward_output[:4] if self.aux_loss_enabled: aux_loss = forward_output[4] # if reference_logps in batch use them, otherwise use the reference model if "reference_logps" in batch: chosen_idx = [i for i in range(batch["reference_logps"].shape[0]) if batch["label"][i] is True] rejected_idx = [i for i in range(batch["reference_logps"].shape[0]) if batch["label"][i] is False] reference_chosen_logps = batch["reference_logps"][chosen_idx, ...] reference_rejected_logps = batch["reference_logps"][rejected_idx, ...] else: with torch.no_grad(): if self.ref_model is None: with self.null_ref_context(): ( reference_chosen_logps, reference_rejected_logps, _, _, ) = self.forward(self.model, batch)[:4] else: ( reference_chosen_logps, reference_rejected_logps, _, _, ) = self.forward(self.ref_model, batch)[:4] chosen_embeddings, rejected_embeddings = self._get_prompt_embeddings(batch) losses, chosen_rewards, rejected_rewards, delta = self.bco_loss( policy_chosen_logps, policy_rejected_logps, reference_chosen_logps, reference_rejected_logps, chosen_embeddings, rejected_embeddings, do_train=do_train, ) metrics["delta"] = self.accelerator.gather_for_metrics(delta).mean().item() num_chosen = torch.Tensor([len(chosen_rewards)]).to(self.accelerator.device) num_rejected = torch.Tensor([len(rejected_rewards)]).to(self.accelerator.device) all_num_chosen = self.accelerator.gather_for_metrics(num_chosen).sum().item() all_num_rejected = self.accelerator.gather_for_metrics(num_rejected).sum().item() if all_num_chosen > 0: metrics["rewards/chosen_sum"] = ( self.accelerator.gather_for_metrics(chosen_rewards.nansum()).nansum().item() ) metrics["logps/chosen_sum"] = ( self.accelerator.gather_for_metrics(policy_chosen_logps.nansum()).nansum().item() ) metrics["logits/chosen_sum"] = ( self.accelerator.gather_for_metrics(policy_chosen_logits.nansum()).nansum().item() ) metrics["count/chosen"] = all_num_chosen if all_num_rejected > 0: metrics["rewards/rejected_sum"] = ( self.accelerator.gather_for_metrics(rejected_rewards.nansum()).nansum().item() ) metrics["logps/rejected_sum"] = ( self.accelerator.gather_for_metrics(policy_rejected_logps.nansum()).nansum().item() ) metrics["logits/rejected_sum"] = ( self.accelerator.gather_for_metrics(policy_rejected_logits.nansum()).nansum().item() ) metrics["count/rejected"] = all_num_rejected loss = losses.nanmean() if self.aux_loss_enabled: loss += self.aux_loss_coef * aux_loss return loss, metrics def compute_loss( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], return_outputs=False, num_items_in_batch=None, ) -> torch.Tensor | tuple[torch.Tensor, dict[str, torch.Tensor]]: compute_loss_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with compute_loss_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs) # Make sure to move the loss to the device the original accumulating loss is at back in the `Trainer` class: loss = loss.to(self.args.device) # force log the metrics if self.accelerator.is_main_process: self.store_metrics(metrics, train_eval="train") if return_outputs: return (loss, metrics) return loss def store_metrics(self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train") -> None: for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) def _get_train_sampler(self, dataset: Dataset | None = None) -> torch.utils.data.Sampler | None: if dataset is None: dataset = self.train_dataset if dataset is None or not has_length(dataset): return None return SequentialSampler(dataset) def generate_from_model_and_ref(self, model, batch: dict[str, torch.LongTensor]) -> tuple[str, str]: """Generate samples from the model and reference model for the given batch of inputs.""" # If one uses `generate_during_eval` with peft + bf16, we need to explicitly call generate with # the torch amp context manager as some hidden states are silently casted to full precision. generate_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with generate_context_manager: policy_output = model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) # if reference_output in batch use that otherwise use the reference model if "reference_output" in batch: reference_output = batch["reference_output"] else: if self.ref_model is None: with self.null_ref_context(): reference_output = self.model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) else: reference_output = self.ref_model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) policy_output = pad_to_length(policy_output, self.max_length, self.processing_class.pad_token_id) policy_output_decoded = self.processing_class.batch_decode(policy_output, skip_special_tokens=True) reference_output = pad_to_length(reference_output, self.max_length, self.processing_class.pad_token_id) reference_output_decoded = self.processing_class.batch_decode(reference_output, skip_special_tokens=True) return policy_output_decoded, reference_output_decoded def prediction_step( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], prediction_loss_only: bool, ignore_keys: list[str] | None = None, ): if ignore_keys is None: if hasattr(model, "config"): ignore_keys = getattr(model.config, "keys_to_ignore_at_inference", []) else: ignore_keys = [] prediction_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with torch.no_grad(), prediction_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs, do_train=False) # force log the metrics if self.accelerator.is_main_process: self.store_metrics(metrics, train_eval="eval") if prediction_loss_only: return (loss.detach(), None, None) # logits for the chosen and rejected samples from model logits_dict = {} if "logits/chosen_sum" in metrics: logits_dict["eval_logits/chosen"] = metrics["logits/chosen_sum"] if "logits/rejected_sum" in metrics: logits_dict["eval_logits/rejected"] = metrics["logits/rejected_sum"] logits = [v for k, v in logits_dict.items() if k not in ignore_keys] logits = torch.tensor(logits, device=self.accelerator.device) labels = torch.zeros(logits.shape[0], device=self.accelerator.device) return (loss.detach(), logits, labels) def evaluation_loop( self, dataloader: DataLoader, description: str, prediction_loss_only: bool | None = None, ignore_keys: list[str] | None = None, metric_key_prefix: str = "eval", ) -> EvalLoopOutput: """ Overriding built-in evaluation loop to store metrics for each batch. Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. Works both with or without labels. """ # Sample and save to game log if requested (for one batch to save time) if self.generate_during_eval: # Generate random indices within the range of the total number of samples num_samples = len(dataloader.dataset) random_indices = random.sample(range(num_samples), k=self.args.eval_batch_size) # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader random_batch_dataset = dataloader.dataset.select(random_indices) random_batch = self.data_collator(random_batch_dataset) random_batch = self._prepare_inputs(random_batch) target_labels = torch.tensor(random_batch["label"], dtype=torch.bool, device=self.accelerator.device) target_indices = torch.where(~target_labels)[0] target_batch = { "prompt_input_ids": random_batch["prompt_input_ids"][target_indices], "prompt_attention_mask": random_batch["prompt_attention_mask"][target_indices], "prompt": itemgetter(*target_indices)(random_batch["prompt"]), } policy_output_decoded, ref_output_decoded = self.generate_from_model_and_ref(self.model, target_batch) table = pd.DataFrame( columns=["Prompt", "Policy", "Ref Model"], data=[ [prompt, pol[len(prompt) :], ref[len(prompt) :]] for prompt, pol, ref in zip( target_batch["prompt"], policy_output_decoded, ref_output_decoded, strict=True ) ], ) if "wandb" in self.args.report_to: wandb.log({"game_log": wandb.Table(data=table)}) if "comet_ml" in self.args.report_to: log_table_to_comet_experiment( name="game_log.csv", table=table, ) # Base evaluation initial_output = super().evaluation_loop( dataloader, description, prediction_loss_only, ignore_keys, metric_key_prefix ) return initial_output def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: logs (`dict[str, float]`): The values to log. start_time (`float`, *optional*): Start time of the training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" # train metrics should have no prefix, eval should have 'eval_' prefix = "eval_" if train_eval == "eval" else "" # accumulate average metrics from sums and lengths for split in ["chosen", "rejected"]: if f"count/{split}" in self._stored_metrics[train_eval]: count_sum = torch.Tensor(self._stored_metrics[train_eval][f"count/{split}"]).sum().item() for metric in ["rewards", "logps", "logits"]: logs[f"{prefix}{metric}/{split}"] = ( torch.Tensor(self._stored_metrics[train_eval][f"{metric}/{split}_sum"]).sum().item() / count_sum ) # delete obsolete metric del self._stored_metrics[train_eval][f"{metric}/{split}_sum"] del self._stored_metrics[train_eval][f"count/{split}"] # calculate reward margin if f"{prefix}rewards/chosen" in logs and f"{prefix}rewards/rejected" in logs: logs[f"{prefix}rewards/margins"] = logs[f"{prefix}rewards/chosen"] - logs[f"{prefix}rewards/rejected"] # Add averaged stored metrics to logs for key, metrics in self._stored_metrics[train_eval].items(): logs[f"{prefix}{key}"] = torch.Tensor(metrics).mean().item() del self._stored_metrics[train_eval] return super().log(logs, start_time) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/bema_for_ref_model/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .callback import BEMACallback from .dpo_trainer import DPOTrainer ================================================ FILE: trl/experimental/bema_for_ref_model/callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import torch from transformers import PreTrainedModel, TrainerControl, TrainerState, TrainingArguments from transformers.trainer_callback import CallbackHandler from ...trainer.callbacks import BEMACallback as _BEMACallback # Logger for module-level logging logger = logging.getLogger(__name__) class CallbackHandlerWithRefModel(CallbackHandler): """ A [`~transformers.CallbackHandler`] that supports passing a reference model to callbacks. """ def __init__(self, callbacks, model, ref_model, processing_class, optimizer, lr_scheduler): super().__init__(callbacks, model, processing_class, optimizer, lr_scheduler) self.ref_model = ref_model # Copied from CallbackHandler.call_event with the addition of `ref_model` to the callback call. def call_event(self, event, args, state, control, **kwargs): for callback in self.callbacks: result = getattr(callback, event)( args, state, control, model=self.model, ref_model=self.ref_model, # <- Added ref_model to the callback call processing_class=self.processing_class, optimizer=self.optimizer, lr_scheduler=self.lr_scheduler, train_dataloader=self.train_dataloader, eval_dataloader=self.eval_dataloader, **kwargs, ) # A Callback can skip the return of `control` if it doesn't change it. if result is not None: control = result return control class BEMACallback(_BEMACallback): # docstyle-ignore r""" A [`~transformers.TrainerCallback`] that implements [BEMA](https://huggingface.co/papers/2508.00180) (Bias-Corrected Exponential Moving Average) by [Adam Block](https://huggingface.co/abblock) and [Cyril Zhang](https://huggingface.co/cyrilzhang). Code from https://github.com/abblock/bema under MIT license. BEMA computes model weights that scale like: $$ \theta_t' = \alpha_t \cdot (\theta_t - \theta_0) + \text{EMA}_t $$ where \\( \theta_t \\) is the current model weights, \\( \theta_0 \\) is a snapshot of the model weights at the first `update_after` step, \\( \text{EMA}_t \\) is the exponential moving average of the model weights, and \\( \alpha_t \\) is a scaling factor that decays with the number of steps \\( t \\) as $$ \alpha_t = (\rho + \gamma \cdot t)^{-\eta}. $$ The EMA is computed as: $$ \text{EMA}_t = (1 - \beta_t) \cdot \text{EMA}_{t-1} + \beta_t \cdot \theta_t $$ where \\( \beta_t \\) is a decay factor that decays with the number of steps \\( t \\) as $$ \beta_t = (\rho + \gamma \cdot t)^{-\kappa}. $$ Args: update_freq (`int`, *optional*, defaults to `400`): Update the BEMA weights every X steps. Denoted this as \\( \phi \\) in the paper. ema_power (`float`, *optional*, defaults to `0.5`): Power for the EMA decay factor. Denoted \\( \kappa \\) in the paper. To disable EMA, set this to `0.0`. bias_power (`float`, *optional*, defaults to `0.2`): Power for the BEMA scaling factor. Denoted \\( \eta \\) in the paper. To disable BEMA, set this to `0.0`. lag (`int`, *optional*, defaults to `10`): Initial offset in the weight decay schedule that controls early-stage smoothness by acting as a virtual starting age for the updates. Denoted as \\( \rho \\) in the paper. update_after (`int`, *optional*, defaults to `0`): Burn-in time before starting to update the BEMA weights. Denoted \\( \tau \\) in the paper. multiplier (`float`, *optional*, defaults to `1.0`): Initial value for the EMA decay factor. Denoted as \\( \gamma \\) in the paper. min_ema_multiplier (`float`, *optional*, defaults to `0.0`): Minimum value for the EMA decay factor. device (`str`, *optional*, defaults to `"cpu"`): Device to use for the BEMA buffers, e.g. `"cpu"` or `"cuda"`. Note that in most cases, this device SHOULD BE DIFFERENT from the device used for training in order to avoid OOM. update_ref_model (`bool`, *optional*, defaults to `False`): Whether to update the reference model with BEMA weights. This creates a lagged, smoothed version of the main model as the reference model. ref_model_update_freq (`int`, *optional*, defaults to `400`): Update the reference model with BEMA weights every this many steps. ref_model_update_after (`int`, *optional*, defaults to `0`): Number of steps to wait before starting to update the reference model. Example: ```python from trl import BEMACallback trainer = Trainer(..., callbacks=[BEMACallback()]) ``` """ def __init__( self, update_freq: int = 400, ema_power: float = 0.5, bias_power: float = 0.2, lag: int = 10, update_after: int = 0, multiplier: float = 1.0, min_ema_multiplier: float = 0.0, device: str = "cpu", update_ref_model: bool = False, ref_model_update_freq: int = 400, ref_model_update_after: int = 0, ): super().__init__( update_freq, ema_power, bias_power, lag, update_after, multiplier, min_ema_multiplier, device, ) # Reference model update parameters self.update_ref_model = update_ref_model self.ref_model_update_freq = ref_model_update_freq self.ref_model_update_after = ref_model_update_after @torch.no_grad() def on_step_end( self, args: TrainingArguments, state: TrainerState, control: TrainerControl, model: PreTrainedModel, **kwargs ): super().on_step_end(args, state, control, model, **kwargs) step = state.global_step # Update reference model if enabled if ( self.update_ref_model and step >= self.ref_model_update_after and (step - self.ref_model_update_after) % self.ref_model_update_freq == 0 ): if "ref_model" not in kwargs: raise ValueError("'ref_model' not found in kwargs.") ref_model = kwargs["ref_model"] # Get the current BEMA state dict bema_state_dict = self.running_model.state_dict() # Handle the case where ref_model is None (PEFT case) if ref_model is None: # In PEFT case, ref_model is None and we need to update the base model of the main model main_model = self._unwrap_model(model) if hasattr(main_model, "get_base_model"): # This is a PEFT model, update the base model base_model = main_model.get_base_model() self._update_model_with_bema_weights(base_model, bema_state_dict, is_peft_base=True) else: # Regular model, update directly self._update_model_with_bema_weights(main_model, bema_state_dict, is_peft_base=False) else: # ref_model is provided, unwrap it and update ref_model = self._unwrap_model(ref_model) if hasattr(ref_model, "get_base_model"): # This is a PEFT model, update the base model base_model = ref_model.get_base_model() self._update_model_with_bema_weights(base_model, bema_state_dict, is_peft_base=True) else: # Regular model, update directly self._update_model_with_bema_weights(ref_model, bema_state_dict, is_peft_base=False) logger.info("BEMACallback: Updated reference model with BEMA weights") def _update_model_with_bema_weights(self, model, bema_state_dict, is_peft_base=False): """Helper method to update a model with BEMA weights, handling PEFT and distributed scenarios.""" if is_peft_base: # For PEFT base models, filter out adapter parameters filtered_state_dict = {} for key, value in bema_state_dict.items(): # Skip adapter parameters if not key.startswith("lora_") and not key.startswith("adapter_"): # Remove 'base_model.' prefix if it exists if key.startswith("base_model."): base_key = key[len("base_model.") :] else: base_key = key filtered_state_dict[base_key] = value # Update the base model model.load_state_dict(filtered_state_dict, strict=False) else: # Regular model, update directly model.load_state_dict(bema_state_dict, strict=False) ================================================ FILE: trl/experimental/bema_for_ref_model/dpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from ...trainer.dpo_trainer import DPOTrainer as _DPOTrainer from .callback import CallbackHandlerWithRefModel class DPOTrainer(_DPOTrainer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Replace with a new one that calls the events with the reference model self.callback_handler = CallbackHandlerWithRefModel( self.callback_handler.callbacks, self.model, self.ref_model, self.processing_class, self.optimizer, self.lr_scheduler, ) ================================================ FILE: trl/experimental/cpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .cpo_config import CPOConfig from .cpo_trainer import CPOTrainer __all__ = ["CPOConfig", "CPOTrainer"] ================================================ FILE: trl/experimental/cpo/cpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from ...trainer.base_config import _BaseConfig @dataclass class CPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.cpo.CPOTrainer`]. This class includes only the parameters that are specific to CPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the sequences (prompt + completion) in the batch. This argument is required if you want to use the default data collator. max_completion_length (`int`, *optional*): Maximum length of the completion. This argument is required if you want to use the default data collator and your model is an encoder-decoder. beta (`float`, *optional*, defaults to `0.1`): Parameter controlling the deviation from the reference model. Higher β means less deviation from the reference model. For the IPO loss (`loss_type="ipo"`), β is the regularization parameter denoted by τ in the [paper](https://huggingface.co/papers/2310.12036). label_smoothing (`float`, *optional*, defaults to `0.0`): Label smoothing factor. This argument is required if you want to use the default data collator. loss_type (`str`, *optional*, defaults to `"sigmoid"`): Type of loss to use. Possible values are: - `"sigmoid"`: sigmoid loss from the original [DPO](https://huggingface.co/papers/2305.18290) paper. - `"hinge"`: hinge loss on the normalized likelihood from the [SLiC](https://huggingface.co/papers/2305.10425) paper. - `"ipo"`: IPO loss from the [IPO](https://huggingface.co/papers/2310.12036) paper. - `"simpo"`: SimPO loss from the [SimPO](https://huggingface.co/papers/2405.14734) paper. - `"alphapo"`: AlphaPO loss from the [AlphaPO](https://huggingface.co/papers/2501.03884) paper. This automatically sets `loss_type="simpo"` and `cpo_alpha=0.0`. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. cpo_alpha (`float`, *optional*, defaults to `1.0`): Weight of the BC regularizer in CPO training. simpo_gamma (`float`, *optional*, defaults to `0.5`): Target reward margin for the SimPO loss, used only when the `loss_type="simpo"`. alpha (`float`, *optional*, defaults to `0.0`): Alpha parameter that controls reward function shape across all loss types. When alpha=0 (default), uses standard log probability rewards. When `alpha != 0`, applies AlphaPO transformation: `r = (1 - p^(-alpha)) / alpha` from the [AlphaPO paper](https://huggingface.co/papers/2501.03884). This parameter works with all loss types. truncation_mode (`str`,*optional*, defaults to `"keep_end"`): Truncation mode to use when the prompt is too long. Possible values are `"keep_end"` or `"keep_start"`. This argument is required if you want to use the default data collator. generate_during_eval (`bool`, *optional*, defaults to `False`): If `True`, generates and logs completions from the model to W&B or Comet during evaluation. is_encoder_decoder (`bool`, *optional*): When using the `model_init` argument (callable) to instantiate the model instead of the `model` argument, you need to specify if the model returned by the callable is an encoder-decoder model. model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model from a string. dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) max_length: int | None = field( default=1024, metadata={"help": "Maximum length of the sequences (prompt + completion) in the batch."}, ) max_completion_length: int | None = field( default=None, metadata={ "help": "Maximum length of the completion. This argument is required if you want to use the default data " "collator and your model is an encoder-decoder." }, ) beta: float = field( default=0.1, metadata={ "help": "Parameter controlling the deviation from the reference model. Higher β means less deviation from " "the reference model." }, ) label_smoothing: float = field( default=0.0, metadata={"help": "Label smoothing factor."}, ) loss_type: str = field( default="sigmoid", metadata={ "help": "Type of loss to use.", "choices": ["sigmoid", "hinge", "ipo", "simpo", "alphapo"], }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model."}, ) cpo_alpha: float = field( default=1.0, metadata={"help": "Weight of the BC regularizer in CPO training."}, ) simpo_gamma: float = field( default=0.5, metadata={"help": "Target reward margin for the SimPO loss, used only when the `loss_type='simpo'`."}, ) alpha: float = field( default=0.0, metadata={ "help": "Alpha parameter that controls reward function shape across all loss types. When alpha=0 " "(default), uses standard log probability rewards. When `alpha != 0`, applies AlphaPO transformation: " "`r = (1 - p^(-alpha)) / alpha` from the AlphaPO paper. This parameter works with all loss types." }, ) truncation_mode: str = field( default="keep_end", metadata={ "help": "Truncation mode to use when the prompt is too long.", "choices": ["keep_end", "keep_start"], }, ) generate_during_eval: bool = field( default=False, metadata={"help": "If `True`, generates and logs completions from the model to W&B during evaluation."}, ) is_encoder_decoder: bool | None = field( default=None, metadata={"help": "Whether the model is an encoder-decoder model."}, ) model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model " "from a string." }, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) def __post_init__(self): # Syntactic sugar for AlphaPO: set loss_type to "simpo" and cpo_alpha to 0.0 if self.loss_type == "alphapo": self.loss_type = "simpo" self.cpo_alpha = 0.0 super().__post_init__() ================================================ FILE: trl/experimental/cpo/cpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import random import textwrap from collections import defaultdict from collections.abc import Callable from contextlib import nullcontext from pathlib import Path from typing import Any, Literal import numpy as np import pandas as pd import torch import torch.nn as nn import torch.nn.functional as F import transformers from accelerate import PartialState, logging from datasets import Dataset from packaging.version import Version from torch import autocast from torch.utils.data import DataLoader from transformers import ( AutoModelForCausalLM, BaseImageProcessor, DataCollator, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, is_comet_available, is_wandb_available, ) from transformers.trainer_utils import EvalLoopOutput from transformers.utils import is_peft_available, is_torch_fx_proxy from ...data_utils import maybe_apply_chat_template, maybe_extract_prompt from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import disable_dropout_in_model, log_table_to_comet_experiment, selective_log_softmax from ..utils import ( DPODataCollatorWithPadding, add_bos_token_if_needed, add_eos_token_if_needed, pad_to_length, peft_module_casting_to_bf16, ) from .cpo_config import CPOConfig if is_peft_available(): from peft import PeftModel, get_peft_model, prepare_model_for_kbit_training if is_wandb_available(): import wandb logger = logging.get_logger(__name__) class CPOTrainer(_BaseTrainer): r""" Initialize CPOTrainer. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an [`~transformers.AutoModelForSequenceClassification`]. args ([`experimental.cpo.CPOConfig`]): The CPO config arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. model_init (`Callable[[], transformers.PreTrainedModel]`): The model initializer to use for training. If None is specified, the default model initializer will be used. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. peft_config (`dict`, defaults to `None`): The PEFT configuration to use for training. If you pass a PEFT configuration, the model will be wrapped in a PEFT model. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. """ _tag_names = ["trl", "cpo"] _name = "CPO" _paper = { "title": "Contrastive Preference Optimization: Pushing the Boundaries of LLM Performance in Machine Translation", "id": "2401.08417", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{xu2024contrastive, title = {{Contrastive Preference Optimization: Pushing the Boundaries of LLM Performance in Machine Translation}}, author = {Haoran Xu and Amr Sharaf and Yunmo Chen and Weiting Tan and Lingfeng Shen and Benjamin Van Durme and Kenton Murray and Young Jin Kim}, year = 2024, booktitle = {Forty-first International Conference on Machine Learning, {ICML} 2024, Vienna, Austria, July 21-27, 2024}, publisher = {OpenReview.net}, url = {https://openreview.net/forum?id=51iwkioZpn} }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str | None = None, args: CPOConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, model_init: Callable[[], PreTrainedModel] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalLoopOutput], dict] | None = None, ): if train_dataset is None: raise ValueError("`train_dataset` is required") if args.model_init_kwargs is None: model_init_kwargs = {} elif not isinstance(model, str): raise ValueError("You passed model_kwargs to the CPOTrainer. But your model is already instantiated.") else: model_init_kwargs = args.model_init_kwargs dtype = model_init_kwargs.get("dtype", "auto") if dtype is not None: # Convert to `torch.dtype` if an str is passed if isinstance(dtype, str) and dtype != "auto": dtype = getattr(torch, dtype) if dtype != "auto" and not isinstance(dtype, torch.dtype): raise ValueError( f"Invalid `dtype` passed to the CPOConfig. Expected a string with either `torch.dtype` or 'auto', but got {dtype}." ) model_init_kwargs["dtype"] = dtype model_init_kwargs["device_map"] = model_init_kwargs.get("device_map", "auto") if isinstance(model, str): model = AutoModelForCausalLM.from_pretrained(model, **model_init_kwargs) # Initialize this variable to False. This helps tracking the case when `peft_module_casting_to_bf16` # has been called in order to properly call autocast if needed. self._peft_has_been_casted_to_bf16 = False if not is_peft_available() and peft_config is not None: raise ValueError( "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it to use the PEFT models" ) elif is_peft_available() and peft_config is not None: if isinstance(model, PeftModel): raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first " "merge and unload the existing adapter, save the resulting base model, and then pass that base " "model along with the new `peft_config` to the trainer." ) if getattr(model, "is_loaded_in_8bit", False) or getattr(model, "is_loaded_in_4bit", False): _support_gc_kwargs = hasattr( args, "gradient_checkpointing_kwargs" ) and "gradient_checkpointing_kwargs" in list( inspect.signature(prepare_model_for_kbit_training).parameters ) prepare_model_kwargs = {"use_gradient_checkpointing": args.gradient_checkpointing} if _support_gc_kwargs: prepare_model_kwargs["gradient_checkpointing_kwargs"] = args.gradient_checkpointing_kwargs model = prepare_model_for_kbit_training(model, **prepare_model_kwargs) elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) # get peft model with the given config model = get_peft_model(model, peft_config) if args.bf16 and getattr(model, "is_loaded_in_4bit", False): peft_module_casting_to_bf16(model) # If args.bf16 we need to explicitly call `generate` with torch amp autocast context manager self._peft_has_been_casted_to_bf16 = True # For models that use gradient_checkpointing, we need to attach a hook that enables input # to explicitly have `requires_grad=True`, otherwise training will either silently # fail or completely fail. elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) if args.generate_during_eval and not (is_wandb_available() or is_comet_available()): raise ValueError( "`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve." ) if model is not None: self.is_encoder_decoder = model.config.is_encoder_decoder elif args.is_encoder_decoder is None: raise ValueError("When no model is provided, you need to pass the parameter is_encoder_decoder.") else: self.is_encoder_decoder = args.is_encoder_decoder if self.is_encoder_decoder: self.decoder_start_token_id = model.config.decoder_start_token_id self.pad_token_id = model.config.pad_token_id if processing_class is None: raise ValueError("processing_class must be specified to tokenize a CPO dataset.") if args.max_length is None: logger.warning( "`max_length` is not set in the CPOConfig's init" " it will default to `512` by default, but you should do it yourself in the future.", ) max_length = 512 else: max_length = args.max_length if args.max_completion_length is None and self.is_encoder_decoder: logger.warning( "When using an encoder decoder architecture, you should set `max_completion_length` in the CPOConfig's init" " it will default to `128` by default, but you should do it yourself in the future.", ) max_completion_length = 128 else: max_completion_length = args.max_completion_length if data_collator is None: data_collator = DPODataCollatorWithPadding( pad_token_id=processing_class.pad_token_id, is_encoder_decoder=self.is_encoder_decoder, ) if args.remove_unused_columns: args.remove_unused_columns = False # warn users logger.warning( "When using DPODataCollatorWithPadding, you should set `remove_unused_columns=False` in your TrainingArguments" " we have set it for you, but you should do it yourself in the future.", ) self.use_dpo_data_collator = True else: self.use_dpo_data_collator = False # Disable dropout in the model if args.disable_dropout: disable_dropout_in_model(model) self.max_length = max_length self.generate_during_eval = args.generate_during_eval self.truncation_mode = args.truncation_mode self.max_completion_length = max_completion_length self.processing_class = processing_class if processing_class.pad_token is None: processing_class.pad_token = processing_class.eos_token self.pad_token_id = processing_class.pad_token_id if args.loss_type in ["hinge", "ipo"] and args.label_smoothing > 0: logger.warning( f"You are using the {args.loss_type} loss type that does not support label smoothing. The " "`label_smoothing` parameter will be ignored. Set `label_smoothing` to `0.0` to remove this warning.", ) if args.loss_type == "kto_pair": raise ValueError("Support for kto_pair has been removed in CPOTrainer. Please use KTOTrainer.") self.beta = args.beta self.label_smoothing = args.label_smoothing self.loss_type = args.loss_type self.cpo_alpha = args.cpo_alpha self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) self.aux_loss_coef = getattr(model.config, "router_aux_loss_coef", 0.0) if self.aux_loss_enabled and self.aux_loss_coef == 0.0: logger.warning( "You set `output_router_logits` to `True` in the model config, but `router_aux_loss_coef` is set to " "`0.0`, meaning the auxiliary loss will not be used. Either set `router_aux_loss_coef` to a value " "greater than `0.0`, or set `output_router_logits` to `False` if you don't want to use the auxiliary " "loss.", ) if args.loss_type == "simpo": self.simpo_gamma = args.simpo_gamma # AlphaPO parameter for reward shaping self.alpha = args.alpha self._stored_metrics = defaultdict(lambda: defaultdict(list)) # Compute that only on the main process for faster data processing. # see: https://github.com/huggingface/trl/pull/1255 with PartialState().main_process_first(): # Extract the prompt if needed, and apply the chat template if needed train_dataset = train_dataset.map(maybe_extract_prompt, num_proc=args.dataset_num_proc) train_dataset = train_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc ) if eval_dataset is not None: eval_dataset = eval_dataset.map(maybe_extract_prompt, num_proc=args.dataset_num_proc) eval_dataset = eval_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc, ) # tokenize the dataset train_dataset = train_dataset.map(self.tokenize_row, num_proc=args.dataset_num_proc) if eval_dataset is not None: eval_dataset = eval_dataset.map(self.tokenize_row, num_proc=args.dataset_num_proc) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, model_init=model_init, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) if not hasattr(self, "accelerator"): raise AttributeError( "Your `Trainer` does not have an `accelerator` object. Consider upgrading `transformers`." ) def build_tokenized_answer(self, prompt, answer): """ Llama tokenizer does satisfy `enc(a + b) = enc(a) + enc(b)`. It does ensure `enc(a + b) = enc(a) + enc(a + b)[len(enc(a)):]`. Reference: https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 """ full_tokenized = self.processing_class(prompt + answer, add_special_tokens=False) prompt_input_ids = self.processing_class(prompt, add_special_tokens=False)["input_ids"] answer_input_ids = full_tokenized["input_ids"][len(prompt_input_ids) :] answer_attention_mask = full_tokenized["attention_mask"][len(prompt_input_ids) :] # Concat tokens to form `enc(a) + enc(a + b)[len(enc(a)):]` full_concat_input_ids = np.concatenate([prompt_input_ids, answer_input_ids]) # Prepare input tokens for token by token comparison full_input_ids = np.array(full_tokenized["input_ids"]) if len(full_input_ids) != len(full_concat_input_ids): raise ValueError("Prompt input ids and answer input ids should have the same length.") # On some tokenizers, like Llama-2 tokenizer, there are occasions where tokens # can be merged together when tokenizing prompt+answer. This could result # on the last token from the prompt being different when tokenized on its own # vs when done as prompt+answer. response_token_ids_start_idx = len(prompt_input_ids) # If tokenized prompt is different than both prompt+answer, then it means the # last token has changed due to merging. if prompt_input_ids != full_tokenized["input_ids"][:response_token_ids_start_idx]: response_token_ids_start_idx -= 1 prompt_input_ids = full_tokenized["input_ids"][:response_token_ids_start_idx] prompt_attention_mask = full_tokenized["attention_mask"][:response_token_ids_start_idx] if len(prompt_input_ids) != len(prompt_attention_mask): raise ValueError("Prompt input ids and attention mask should have the same length.") answer_input_ids = full_tokenized["input_ids"][response_token_ids_start_idx:] answer_attention_mask = full_tokenized["attention_mask"][response_token_ids_start_idx:] return dict( prompt_input_ids=prompt_input_ids, prompt_attention_mask=prompt_attention_mask, input_ids=answer_input_ids, attention_mask=answer_attention_mask, ) def tokenize_row(self, feature, model: PreTrainedModel | nn.Module | None = None) -> dict: """Tokenize a single row from a CPO specific dataset. At this stage, we don't convert to PyTorch tensors yet; we just handle the truncation in case the prompt + chosen or prompt + rejected responses is/are too long. First we truncate the prompt; if we're still too long, we truncate the chosen/rejected. We also create the labels for the chosen/rejected responses, which are of length equal to the sum of the length of the prompt and the chosen/rejected response, with `-100` for the prompt tokens. """ batch = {} prompt = feature["prompt"] chosen = feature["chosen"] rejected = feature["rejected"] if not self.is_encoder_decoder: # Check issues below for more details # 1. https://github.com/huggingface/trl/issues/907 # 2. https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 # 3. https://github.com/LianjiaTech/BELLE/issues/337 if not isinstance(prompt, str): raise ValueError(f"prompt should be an str but got {type(prompt)}") prompt_tokens = self.processing_class(prompt, add_special_tokens=False) prompt_tokens = {f"prompt_{k}": v for k, v in prompt_tokens.items()} if not isinstance(chosen, str): raise ValueError(f"chosen should be an str but got {type(chosen)}") chosen_tokens = self.build_tokenized_answer(prompt, chosen) if not isinstance(rejected, str): raise ValueError(f"rejected should be an str but got {type(rejected)}") rejected_tokens = self.build_tokenized_answer(prompt, rejected) # Last prompt token might get merged by tokenizer and # it should not be included for generation if that happens prompt_len_input_ids = len(prompt_tokens["prompt_input_ids"]) chosen_prompt_len_input_ids = len(chosen_tokens["prompt_input_ids"]) rejected_prompt_len_input_ids = len(rejected_tokens["prompt_input_ids"]) prompt_len_input_ids = min(chosen_prompt_len_input_ids, rejected_prompt_len_input_ids) for k, v in prompt_tokens.items(): prompt_tokens[k] = v[:prompt_len_input_ids] # Make sure prompts only have one different token at most an # and length only differs by 1 at most num_diff_tokens = sum( a != b for a, b in zip(chosen_tokens["prompt_input_ids"], rejected_tokens["prompt_input_ids"], strict=False) ) num_diff_len = abs(chosen_prompt_len_input_ids - rejected_prompt_len_input_ids) if num_diff_tokens > 1 or num_diff_len > 1: raise ValueError( "Chosen and rejected prompt_input_ids might only differ on the " "last token due to tokenizer merge ops." ) # add BOS token to head of prompt. Avoid adding if it's already there prompt_tokens, chosen_tokens, rejected_tokens = add_bos_token_if_needed( self.processing_class.bos_token_id, prompt_len_input_ids, prompt_tokens, chosen_prompt_len_input_ids, chosen_tokens, rejected_prompt_len_input_ids, rejected_tokens, ) # add EOS token to end of answer. Avoid adding if it's already there chosen_tokens, rejected_tokens = add_eos_token_if_needed( self.processing_class.eos_token_id, chosen_tokens, rejected_tokens ) longer_response_length = max(len(chosen_tokens["input_ids"]), len(rejected_tokens["input_ids"])) # if combined sequence is too long, truncate the response for answer_tokens in [chosen_tokens, rejected_tokens]: if len(answer_tokens["prompt_input_ids"]) + longer_response_length > self.max_length: for k in ["input_ids", "attention_mask"]: answer_tokens[k] = answer_tokens[k][: self.max_length - longer_response_length] # Create labels chosen_sequence_tokens = { k: chosen_tokens[f"prompt_{k}"] + chosen_tokens[k] for k in ["input_ids", "attention_mask"] } rejected_sequence_tokens = { k: rejected_tokens[f"prompt_{k}"] + rejected_tokens[k] for k in ["input_ids", "attention_mask"] } chosen_sequence_tokens["labels"] = chosen_sequence_tokens["input_ids"][:] chosen_sequence_tokens["labels"][: len(chosen_tokens["prompt_input_ids"])] = [-100] * len( chosen_tokens["prompt_input_ids"] ) rejected_sequence_tokens["labels"] = rejected_sequence_tokens["input_ids"][:] rejected_sequence_tokens["labels"][: len(rejected_tokens["prompt_input_ids"])] = [-100] * len( rejected_tokens["prompt_input_ids"] ) for k, toks in { "chosen_": chosen_sequence_tokens, "rejected_": rejected_sequence_tokens, "": prompt_tokens, }.items(): for type_key, tokens in toks.items(): if type_key == "token_type_ids": continue batch[f"{k}{type_key}"] = tokens else: chosen_tokens = self.processing_class( chosen, truncation=True, max_length=self.max_completion_length, add_special_tokens=True ) rejected_tokens = self.processing_class( rejected, truncation=True, max_length=self.max_completion_length, add_special_tokens=True ) prompt_tokens = self.processing_class(prompt, add_special_tokens=True) batch["chosen_labels"] = chosen_tokens["input_ids"] batch["rejected_labels"] = rejected_tokens["input_ids"] batch["prompt_input_ids"] = prompt_tokens["input_ids"] batch["prompt_attention_mask"] = prompt_tokens["attention_mask"] if model is not None and hasattr(model, "prepare_decoder_input_ids_from_labels"): batch["rejected_decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels( labels=torch.tensor(batch["rejected_labels"]) ) batch["chosen_decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels( labels=torch.tensor(batch["chosen_labels"]) ) return batch @staticmethod def concatenated_inputs( batch: dict[str, list | torch.LongTensor], is_encoder_decoder: bool = False, padding_value: int = 0, device: torch.device | None = None, ) -> dict[str, torch.LongTensor]: """Concatenate the chosen and rejected inputs into a single tensor. Args: batch: A batch of data. Must contain the keys 'chosen_input_ids' and 'rejected_input_ids', which are tensors of shape (batch_size, sequence_length). is_encoder_decoder: Whether the model is an encoder-decoder model. padding_value: The padding value to use for the concatenated inputs_ids. device: The device for the concatenated inputs. Returns: A dictionary containing the concatenated inputs under the key 'concatenated_input_ids'. """ concatenated_batch = {} if is_encoder_decoder: max_length = max(batch["chosen_labels"].shape[1], batch["rejected_labels"].shape[1]) else: max_length = max(batch["chosen_input_ids"].shape[1], batch["rejected_input_ids"].shape[1]) for k in batch: if k.startswith("chosen") and isinstance(batch[k], torch.Tensor): if "labels" in k or is_encoder_decoder: pad_value = -100 elif k.endswith("_input_ids"): pad_value = padding_value elif k.endswith("_attention_mask"): pad_value = 0 concatenated_key = k.replace("chosen", "concatenated") concatenated_batch[concatenated_key] = pad_to_length(batch[k], max_length, pad_value=pad_value) for k in batch: if k.startswith("rejected") and isinstance(batch[k], torch.Tensor): if "labels" in k or is_encoder_decoder: pad_value = -100 elif k.endswith("_input_ids"): pad_value = padding_value elif k.endswith("_attention_mask"): pad_value = 0 concatenated_key = k.replace("rejected", "concatenated") concatenated_batch[concatenated_key] = torch.cat( ( concatenated_batch[concatenated_key], pad_to_length(batch[k], max_length, pad_value=pad_value), ), dim=0, ).to(device=device) if is_encoder_decoder: concatenated_batch["concatenated_input_ids"] = batch["prompt_input_ids"].repeat(2, 1).to(device=device) concatenated_batch["concatenated_attention_mask"] = ( batch["prompt_attention_mask"].repeat(2, 1).to(device=device) ) return concatenated_batch def cpo_loss( self, policy_chosen_logps: torch.FloatTensor, policy_rejected_logps: torch.FloatTensor, ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Compute the CPO loss for a batch of policy and reference model log probabilities. Args: policy_chosen_logps: Log probabilities of the policy model for the chosen responses. Shape: (batch_size,) policy_rejected_logps: Log probabilities of the policy model for the rejected responses. Shape: (batch_size,) Returns: A tuple of three tensors: (losses, chosen_rewards, rejected_rewards). The losses tensor contains the CPO loss for each example in the batch. The chosen_rewards and rejected_rewards tensors contain the rewards for the chosen and rejected responses, respectively. """ # Apply AlphaPO reward transformation if alpha != 0 if self.alpha != 0.0: # Compute probabilities chosen_probs = torch.exp(policy_chosen_logps) rejected_probs = torch.exp(policy_rejected_logps) # Apply AlphaPO transformation: r = (1 - p^(-alpha)) / alpha policy_chosen_rewards = (1 - chosen_probs.pow(-self.alpha)) / self.alpha policy_rejected_rewards = (1 - rejected_probs.pow(-self.alpha)) / self.alpha logits = (policy_chosen_rewards - policy_rejected_rewards).to(self.accelerator.device) else: # Standard log probability rewards when alpha = 0 logits = (policy_chosen_logps - policy_rejected_logps).to(self.accelerator.device) # The beta is a temperature parameter for the CPO loss, typically something in the range of 0.1 to 0.5. # We ignore the reference model as beta -> 0. The label_smoothing parameter encodes our uncertainty about the labels and # calculates a conservative CPO loss. if self.loss_type == "simpo": gamma_logratios = self.simpo_gamma / self.beta logits = logits - gamma_logratios # This reduces to Equation 3 from the CPO paper when label_smoothing -> 0. losses = ( -F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * logits) * self.label_smoothing ) elif self.loss_type == "sigmoid": # This reduces to Equation 3 from the CPO paper when label_smoothing -> 0. losses = ( -F.logsigmoid(self.beta * logits) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * logits) * self.label_smoothing ) elif self.loss_type == "hinge": losses = torch.relu(1 - self.beta * logits) elif self.loss_type == "ipo": # eqn (17) of the paper where beta is the regularization parameter for the IPO loss, denoted by tau in the paper. losses = (logits - 1 / (2 * self.beta)) ** 2 else: raise ValueError( f"Unknown loss type: {self.loss_type}. Should be one of ['sigmoid', 'hinge', 'ipo', 'simpo']" ) # Calculate rewards for logging if self.alpha != 0.0: # When using AlphaPO transformation, use the transformed rewards chosen_rewards = self.beta * policy_chosen_rewards.to(self.accelerator.device).detach() rejected_rewards = self.beta * policy_rejected_rewards.to(self.accelerator.device).detach() else: # Standard log probability rewards chosen_rewards = self.beta * (policy_chosen_logps.to(self.accelerator.device)).detach() rejected_rewards = self.beta * (policy_rejected_logps.to(self.accelerator.device)).detach() return losses, chosen_rewards, rejected_rewards @staticmethod def get_batch_logps( logits: torch.FloatTensor, labels: torch.LongTensor, average_log_prob: bool = False, is_encoder_decoder: bool = False, ) -> torch.FloatTensor: """Compute the log probabilities of the given labels under the given logits. Args: logits: Logits of the model (unnormalized). Shape: (batch_size, sequence_length, vocab_size) labels: Labels for which to compute the log probabilities. Label tokens with a value of `-100` are ignored. Shape: (batch_size, sequence_length) average_log_prob: If True, return the average log probability per (non-masked) token. Otherwise, return the sum of the log probabilities of the (non-masked) tokens. is_encoder_decoder: Whether the model is an encoder-decoder model. Returns: A tensor of shape (batch_size,) containing the average/sum log probabilities of the given labels under the given logits. """ if logits.shape[:-1] != labels.shape: raise ValueError("Logits (batch and sequence length dim) and labels must have the same shape.") if not is_encoder_decoder: labels = labels[:, 1:].clone() logits = logits[:, :-1, :] loss_mask = labels != -100 # dummy token; we'll ignore the losses on these tokens later labels[labels == -100] = 0 per_token_logps = selective_log_softmax(logits, labels) if average_log_prob: return (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) else: return (per_token_logps * loss_mask).sum(-1) def concatenated_forward( self, model: nn.Module, batch: dict[str, list | torch.LongTensor] ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Run the given model on the given batch of inputs, concatenating the chosen and rejected inputs together. We do this to avoid doing two forward passes, because it's faster for FSDP. """ concatenated_batch = self.concatenated_inputs( batch, is_encoder_decoder=self.is_encoder_decoder, padding_value=self.pad_token_id, device=self.accelerator.device, ) len_chosen = batch["chosen_labels"].shape[0] model_kwargs = ( { "decoder_input_ids": self._shift_right(concatenated_batch["concatenated_labels"]), } if self.is_encoder_decoder else {} ) if self.aux_loss_enabled: model_kwargs["output_router_logits"] = True outputs = model( concatenated_batch["concatenated_input_ids"], attention_mask=concatenated_batch["concatenated_attention_mask"], use_cache=False, **model_kwargs, ) all_logits = outputs.logits def cross_entropy_loss(logits, labels): if not self.is_encoder_decoder: # Shift so that tokens < n predict n logits = logits[..., :-1, :].contiguous() labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() logits = logits.view(-1, logits.shape[-1]) labels = labels.view(-1) # Enable model parallelism labels = labels.to(logits.device) loss = loss_fct(logits, labels) return loss labels = concatenated_batch["concatenated_labels"].clone() if self.cpo_alpha == 0: nll_loss = torch.tensor(0.0).to(self.accelerator.device) else: nll_loss = cross_entropy_loss(all_logits[:len_chosen], labels[:len_chosen]) all_logps = self.get_batch_logps( all_logits, concatenated_batch["concatenated_labels"], average_log_prob=self.loss_type in ["ipo", "simpo"], is_encoder_decoder=self.is_encoder_decoder, ) chosen_logps = all_logps[:len_chosen] rejected_logps = all_logps[len_chosen:] chosen_logits = all_logits[:len_chosen] rejected_logits = all_logits[len_chosen:] if self.aux_loss_enabled: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, nll_loss, outputs.aux_loss) return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, nll_loss) def get_batch_loss_metrics( self, model, batch: dict[str, list | torch.LongTensor], train_eval: Literal["train", "eval"] = "train", ): """Compute the CPO loss and other metrics for the given batch of inputs for train or test.""" metrics = {} forward_output = self.concatenated_forward(model, batch) ( policy_chosen_logps, policy_rejected_logps, policy_chosen_logits, policy_rejected_logits, policy_nll_loss, ) = forward_output[:5] if self.aux_loss_enabled: aux_loss = forward_output[5] losses, chosen_rewards, rejected_rewards = self.cpo_loss( policy_chosen_logps, policy_rejected_logps, ) loss = losses.mean() + self.cpo_alpha * policy_nll_loss reward_accuracies = (chosen_rewards > rejected_rewards).float() prefix = "eval_" if train_eval == "eval" else "" metrics[f"{prefix}rewards/chosen"] = self.accelerator.gather_for_metrics(chosen_rewards).mean().item() metrics[f"{prefix}rewards/rejected"] = self.accelerator.gather_for_metrics(rejected_rewards).mean().item() metrics[f"{prefix}rewards/accuracies"] = self.accelerator.gather_for_metrics(reward_accuracies).mean().item() metrics[f"{prefix}rewards/margins"] = ( self.accelerator.gather_for_metrics(chosen_rewards - rejected_rewards).mean().item() ) metrics[f"{prefix}logps/rejected"] = ( self.accelerator.gather_for_metrics(policy_rejected_logps).detach().mean().item() ) metrics[f"{prefix}logps/chosen"] = ( self.accelerator.gather_for_metrics(policy_chosen_logps).detach().mean().item() ) metrics[f"{prefix}logits/rejected"] = ( self.accelerator.gather_for_metrics(policy_rejected_logits.detach().mean()).mean().item() ) metrics[f"{prefix}logits/chosen"] = ( self.accelerator.gather_for_metrics(policy_chosen_logits.detach().mean()).mean().item() ) metrics[f"{prefix}nll_loss"] = self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean().item() if self.aux_loss_enabled: loss += self.aux_loss_coef * aux_loss return loss, metrics def compute_loss( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], return_outputs=False, num_items_in_batch=None, ) -> torch.Tensor | tuple[torch.Tensor, dict[str, torch.Tensor]]: compute_loss_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with compute_loss_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs, train_eval="train") # force log the metrics self.store_metrics(metrics, train_eval="train") if return_outputs: return (loss, metrics) return loss def generate_from_model(self, model, batch: dict[str, torch.LongTensor]) -> str: """Generate samples from the model and reference model for the given batch of inputs.""" # If one uses `generate_during_eval` with peft + bf16, we need to explicitly call generate with # the torch amp context manager as some hidden states are silently casted to full precision. generate_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with generate_context_manager: policy_output = model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) policy_output = pad_to_length(policy_output, self.max_length, self.processing_class.pad_token_id) policy_output_decoded = self.processing_class.batch_decode(policy_output, skip_special_tokens=True) return policy_output_decoded def prediction_step( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], prediction_loss_only: bool, ignore_keys: list[str] | None = None, ): if ignore_keys is None: if hasattr(model, "config"): ignore_keys = getattr(model.config, "keys_to_ignore_at_inference", []) else: ignore_keys = [] prediction_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with torch.no_grad(), prediction_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs, train_eval="eval") # force log the metrics self.store_metrics(metrics, train_eval="eval") if prediction_loss_only: return (loss.detach(), None, None) # logits for the chosen and rejected samples from model logits_dict = { "eval_logits/chosen": metrics["eval_logits/chosen"], "eval_logits/rejected": metrics["eval_logits/rejected"], } logits = [v for k, v in logits_dict.items() if k not in ignore_keys] logits = torch.tensor(logits, device=self.accelerator.device) labels = torch.zeros(logits.shape[0], device=self.accelerator.device) return (loss.detach(), logits, labels) def store_metrics(self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train") -> None: for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) def evaluation_loop( self, dataloader: DataLoader, description: str, prediction_loss_only: bool | None = None, ignore_keys: list[str] | None = None, metric_key_prefix: str = "eval", ) -> EvalLoopOutput: """ Overriding built-in evaluation loop to store metrics for each batch. Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. Works both with or without labels. """ # Sample and save to game log if requested (for one batch to save time) if self.generate_during_eval: # Generate random indices within the range of the total number of samples num_samples = len(dataloader.dataset) random_indices = random.sample(range(num_samples), k=self.args.eval_batch_size) # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader random_batch_dataset = dataloader.dataset.select(random_indices) random_batch = self.data_collator(random_batch_dataset) random_batch = self._prepare_inputs(random_batch) policy_output_decoded = self.generate_from_model(self.model, random_batch) table = pd.DataFrame( columns=["Prompt", "Policy"], data=[ [prompt, pol[len(prompt) :]] for prompt, pol in zip(random_batch["prompt"], policy_output_decoded, strict=True) ], ) if "wandb" in self.args.report_to: wandb.log({"game_log": wandb.Table(data=table)}) if "comet_ml" in self.args.report_to: log_table_to_comet_experiment( name="game_log.csv", table=table, ) # Base evaluation initial_output = super().evaluation_loop( dataloader, description, prediction_loss_only, ignore_keys, metric_key_prefix ) return initial_output def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: logs (`dict[str, float]`): The values to log. start_time (`float`, *optional*): Start time of the training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" # Add averaged stored metrics to logs for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] return super().log(logs, start_time) def _shift_right(self, input_ids): if self.decoder_start_token_id is None: raise ValueError( "model.config.decoder_start_token_id has to be defined. It is usually set to the pad_token_id." ) # shift inputs to the right if is_torch_fx_proxy(input_ids): # Item assignment is not supported natively for proxies. shifted_input_ids = torch.full(input_ids.shape[:-1] + (1,), self.decoder_start_token_id) shifted_input_ids = torch.cat([shifted_input_ids, input_ids[..., :-1]], dim=-1) else: shifted_input_ids = input_ids.new_zeros(input_ids.shape) shifted_input_ids[..., 1:] = input_ids[..., :-1].clone() shifted_input_ids[..., 0] = self.decoder_start_token_id if self.pad_token_id is None: raise ValueError("model.config.pad_token_id has to be defined.") # replace possible -100 values in labels by `pad_token_id` shifted_input_ids.masked_fill_(shifted_input_ids == -100, self.pad_token_id) return shifted_input_ids # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/dppo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .dppo_config import DPPOConfig from .dppo_trainer import DPPOTrainer ================================================ FILE: trl/experimental/dppo/dppo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Literal from ...trainer.grpo_config import GRPOConfig @dataclass class DPPOConfig(GRPOConfig): """ Configuration class for DPPOTrainer. DPPO (Divergence Proximal Policy Optimization) replaces PPO/GRPO's heuristic ratio-clipping with a principled trust region based on direct policy divergence estimates. Paper: "Rethinking the Trust Region in LLM Reinforcement Learning" (arXiv:2602.04879) Args: divergence_type (`Literal["binary_tv", "binary_kl", "topk_tv", "topk_kl"]`, *optional*, defaults to `"binary_tv"`): Divergence approximation used for the trust-region mask. Binary variants use only per-token log-probs; top-K variants require storing top-K token IDs and log-probs during rollout generation plus full logits during training. divergence_topk (`int`, *optional*, defaults to `20`): K for top-K divergence approximations. Only used when `divergence_type` is `"topk_tv"` or `"topk_kl"`. clip_ratio_c (`float`, *optional*, defaults to `20.0`): Upper bound on the importance-sampling ratio for stability. The IS ratio is clamped to [0, clip_ratio_c]. epsilon (`float`, inherited from GRPOConfig, default overridden to `0.15`): Divergence threshold δ_low. Tokens whose divergence exceeds this when the policy moves in the advantage-decreasing direction are masked. The paper recommends 0.15 for TV divergence and 0.05 for KL divergence. epsilon_high (`float`, inherited from GRPOConfig, default overridden to `0.15`): Divergence threshold δ_high. Tokens whose divergence exceeds this when the policy moves in the advantage-increasing direction are masked. The paper recommends 0.15 for TV divergence and 0.05 for KL divergence. """ divergence_type: Literal["binary_tv", "binary_kl", "topk_tv", "topk_kl"] = field( default="binary_tv", metadata={ "help": "Divergence approximation used for the trust-region mask. Binary variants use only per-token " "log-probs; top-K variants require storing top-K token IDs and log-probs during rollout generation plus " "full logits during training." }, ) divergence_topk: int = field( default=20, metadata={ "help": "K for top-K divergence approximations. Only used when `divergence_type` is `'topk_tv'` or " "`'topk_kl'`." }, ) clip_ratio_c: float = field( default=20.0, metadata={ "help": "Upper bound on the importance-sampling ratio for stability. The IS ratio is clamped to " "[0, clip_ratio_c]." }, ) epsilon: float = field( default=0.15, metadata={ "help": "Divergence threshold δ_low. Tokens whose divergence exceeds this when the policy moves in the " "advantage-decreasing direction are masked. The paper recommends 0.15 for TV divergence and 0.05 for KL " "divergence." }, ) epsilon_high: float = field( default=0.15, metadata={ "help": "Divergence threshold δ_high. Tokens whose divergence exceeds this when the policy moves in the " "advantage-increasing direction are masked. The paper recommends 0.15 for TV divergence and 0.05 for KL " "divergence." }, ) def __post_init__(self): super().__post_init__() if self.divergence_type not in ("binary_tv", "binary_kl", "topk_tv", "topk_kl"): raise ValueError( f"divergence_type must be one of 'binary_tv', 'binary_kl', 'topk_tv', 'topk_kl', " f"got {self.divergence_type!r}" ) if self.divergence_topk < 1: raise ValueError(f"divergence_topk must be >= 1, got {self.divergence_topk}") if self.clip_ratio_c <= 0: raise ValueError(f"clip_ratio_c must be > 0, got {self.clip_ratio_c}") if self.loss_type != "dapo": raise ValueError(f"loss_type {self.loss_type} is not supported for DPPO") if self.top_entropy_quantile != 1.0: raise ValueError("top_entropy_quantile is not supported for DPPO") if self.off_policy_mask_threshold is not None: raise ValueError("off_policy_mask_threshold is not supported for DPPO") if self.use_transformers_paged: raise ValueError( "DPPO requires sampled token logprobs from the generation backend. " "Transformers paged (`use_transformers_paged=True`) does not support logprob extraction." ) ================================================ FILE: trl/experimental/dppo/dppo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import copy import math import textwrap from collections.abc import Callable from contextlib import nullcontext from copy import copy as shallow_copy from typing import Any import numpy as np import torch import transformers from accelerate.utils import gather_object from datasets import Dataset, IterableDataset from packaging.version import Version from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from transformers import ( PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, Trainer, TrainerCallback, ) from transformers.utils import is_peft_available from ...chat_template_utils import parse_response from ...data_utils import ( apply_chat_template, is_conversational, prepare_multimodal_messages, ) from ...extras.profiling import profiling_context, profiling_decorator from ...models import unwrap_model_for_generation from ...models.utils import disable_gradient_checkpointing from ...trainer.grpo_trainer import EnvironmentFactory, GRPOTrainer, RewardFunc, RolloutFunc from ...trainer.utils import ( entropy_from_logits, nanstd, pad, selective_log_softmax, use_adapter, ) from .dppo_config import DPPOConfig if is_peft_available(): from peft import PeftConfig, PeftModel SAFETY_CLAMP_MAX = 20 def _strip_padding(tensor: torch.Tensor, mask: torch.Tensor) -> list[list]: """Remove padding from a batched tensor using a mask, returning a ragged list-of-lists.""" return [row[m].tolist() for row, m in zip(tensor, mask.bool(), strict=True)] class DPPOTrainer(GRPOTrainer): """ Trainer for Divergence Proximal Policy Optimization (DPPO). DPPO replaces PPO/GRPO's heuristic ratio-clipping with a principled trust region based on direct policy divergence estimates. PPO-style clipping masks tokens based on probability ratio π/μ, which over-penalizes low-probability tokens and under-penalizes high-probability tokens. In contrast, DPPO masks based on direct approximation of policy divergence (e.g TV or KL) ensuring updates stay within a theoretically grounded trust region. Four divergence approximations are supported: - `binary_tv`: Absolute probability difference |π(a) - μ(a)| (simplest) - `binary_kl`: Bernoulli KL divergence between old and new token probabilities - `topk_tv`: Total variation over the top-K tokens of the distribution - `topk_kl`: KL divergence over the top-K tokens of the distribution Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `.from_pretrained` (where `` is derived from the model config) with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. - A [`~peft.PeftModel`] object. Only causal language models are supported. reward_funcs (`RewardFunc | list[RewardFunc]`): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function, such as: - A string: The *model ID* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForSequenceClassification.from_pretrained`] with `num_labels=1` and the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object: Only sequence classification models are supported. - A custom reward function: The function is provided with the prompts and the generated completions, plus any additional columns in the dataset. It should return a list of rewards. Custom reward functions can be either synchronous or asynchronous and can also return `None` when the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returns `None` for a sample, that reward function is excluded from the reward calculation for that sample. For more details, see [Using a custom reward function](#using-a-custom-reward-function). The trainer's state is also passed to the reward function. The trainer's state is an instance of [`~transformers.TrainerState`] and can be accessed by accessing the `trainer_state` argument to the reward function's signature. - A list of reward functions, where each item can independently be any of the above types. Mixing different types within the list (e.g., a string model ID and a custom reward function) is allowed. args ([`DPPOConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. It must include a column `"prompt"`. Any additional columns in the dataset is ignored. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. The padding side must be set to "left". If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. reward_processing_classes ([`~transformers.PreTrainedTokenizerBase`] or `list[PreTrainedTokenizerBase]`, *optional*): Processing classes corresponding to the reward functions specified in `reward_funcs`. Can be either: - A single processing class: Used when `reward_funcs` contains only one reward function. - A list of processing classes: Must match the order and length of the reward functions in `reward_funcs`. If set to `None`, or if an element of the list corresponding to a [`~transformers.PreTrainedModel`] is `None`, the tokenizer for the model is automatically loaded using [`~transformers.AutoTokenizer.from_pretrained`]. For elements in `reward_funcs` that are custom reward functions (not [`~transformers.PreTrainedModel`]), the corresponding entries in `reward_processing_classes` are ignored. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. tools (list of `Callable`, *optional*): A list of callable tool functions (sync or async) that the model can invoke during generation. Each tool should be a standard Python function with properly type-hinted arguments and return values, and a Google-style docstring describing its purpose, arguments, and return value. For more details, see: https://huggingface.co/docs/transformers/en/chat_extras#passing-tools. The model uses the function's name, type hints, and docstring to determine how to call it. Ensure that the model's chat template supports tool use and that it has been fine-tuned for tool calling. rollout_func (`RolloutFunc`, *optional*): Function to use for generating completions. It receives the list of prompts allocated to the current process and the trainer instance. It must return a dict with `"prompt_ids"`, `"completion_ids"`, and `"logprobs"` fields. Any other fields are forwarded to the reward functions. This feature is experimental and may change or be removed at any time without prior notice. """ _tag_names = ["trl", "dppo"] _name = "DPPO" _paper = { "title": "Rethinking the Trust Region in LLM Reinforcement Learning", "id": "2602.04879", # docstyle-ignore "citation": textwrap.dedent("""\ @article{qi2026rethinking, title = {{Rethinking the Trust Region in LLM Reinforcement Learning}}, author = {Qi, Penghui and Zhou, Xiangxin and Liu, Zichen and Pang, Tianyu and Du, Chao and Lin, Min and Lee, Wee Sun}, journal = {arXiv preprint arXiv:2602.04879}, year = {2026} }"""), } def __init__( self, model: "str | PreTrainedModel | PeftModel", reward_funcs: RewardFunc | list[RewardFunc], args: DPPOConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), peft_config: "PeftConfig | None" = None, tools: list[Callable] | None = None, rollout_func: RolloutFunc | None = None, environment_factory: EnvironmentFactory | None = None, ): if args is None: model_name = model if isinstance(model, str) else model.config._name_or_path model_name = model_name.split("/")[-1] args = DPPOConfig(f"{model_name}-DPPO") self.divergence_type = args.divergence_type self.divergence_topk = args.divergence_topk self.clip_ratio_c = args.clip_ratio_c super().__init__( model=model, reward_funcs=reward_funcs, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=reward_processing_classes, callbacks=callbacks, optimizers=optimizers, peft_config=peft_config, tools=tools, rollout_func=rollout_func, environment_factory=environment_factory, ) if self.divergence_type in ["topk_tv", "topk_kl"] and self.use_vllm: self.vllm_generation.logprobs = self.divergence_topk def _tokenize_prompts(self, prompts: list): """Tokenize prompts and extract images/multimodal fields for generation.""" if is_conversational({"prompt": prompts[0]}): images = [] has_images = False for prompt in prompts: prompt_images = [] for message in prompt: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image": prompt_images.append(part["image"]) has_images = True images.append(prompt_images if prompt_images else None) images = images if has_images else None # We pass padding=True to work around a bug introduced in transformers 5.2.0 in some processors # (e.g. Qwen2.5-VL) that crash on batched unpadded input. We then unpad input_ids using attention_mask. # See: https://github.com/huggingface/transformers/issues/44514 tokenized = self.processing_class.apply_chat_template( conversation=prompts, tools=self.tools, chat_template=self.chat_template, add_generation_prompt=True, tokenize=True, return_dict=True, padding=True, **self.chat_template_kwargs, ) prompt_ids = [ [tok for tok, mask in zip(ids, attention_mask, strict=True) if mask] for ids, attention_mask in zip(tokenized["input_ids"], tokenized["attention_mask"], strict=True) ] multimodal_fields = {k: v for k, v in tokenized.items() if k not in ("input_ids", "attention_mask")} else: prompt_ids = self.processing_class(text=prompts)["input_ids"] images = None multimodal_fields = {} return prompt_ids, images, multimodal_fields def _generate_single_turn(self, prompt_ids, images, multimodal_fields): """Generate completions, always extracting sampled token logprobs. Returns: 5-tuple of (prompt_ids, completion_ids, logprobs, topk_logprobs, topk_token_ids). topk_logprobs and topk_token_ids are None when divergence_type is not topk. """ device = self.accelerator.device mode = "train" if self.model.training else "eval" needs_topk = self.divergence_type in ["topk_tv", "topk_kl"] K = self.divergence_topk if self.use_vllm: if self.state.global_step != self._last_loaded_step: with profiling_context(self, "sync_weights"): self.vllm_generation.sync_weights() self._last_loaded_step = self.state.global_step num_generations = self.num_generations if mode == "train" else self.num_generations_eval prompt_ids, completion_ids, logprobs, logprob_token_ids = self.vllm_generation.generate( prompts=prompt_ids, images=images, num_generations=num_generations, profiler=profiling_context(self, "vLLM.generate"), ) if needs_topk: # vLLM returns up to K+1 entries sorted by rank (most probable first). # The sampled token is always included but may be at any position. # Per the paper, A'_t = TopK(μ, K) ∪ {a_t}. We keep exactly K slots: if the # sampled token a_t is not in the top-K, it replaces the K-th ranked entry. topk_logprobs = [] topk_token_ids = [] sampled_logprobs = [] for seq_lps, seq_tids, seq_cids in zip(logprobs, logprob_token_ids, completion_ids, strict=True): seq_topk_lps, seq_topk_tids, seq_sampled = [], [], [] for step_lps, step_tids, sampled_tid in zip(seq_lps, seq_tids, seq_cids, strict=True): idx = step_tids.index(sampled_tid) seq_sampled.append(step_lps[idx]) # Take top-K entries, then ensure sampled token is present tk_lps = step_lps[:K] tk_tids = step_tids[:K] if sampled_tid not in tk_tids: tk_lps[-1] = step_lps[idx] tk_tids[-1] = sampled_tid seq_topk_lps.append(tk_lps) seq_topk_tids.append(tk_tids) topk_logprobs.append(seq_topk_lps) topk_token_ids.append(seq_topk_tids) sampled_logprobs.append(seq_sampled) else: sampled_logprobs = [[step_lps[0] for step_lps in seq_lps] for seq_lps in logprobs] topk_logprobs = None topk_token_ids = None return prompt_ids, completion_ids, sampled_logprobs, topk_logprobs, topk_token_ids else: prompt_tensors = [torch.tensor(ids) for ids in prompt_ids] padded_ids = pad(prompt_tensors, padding_value=self.pad_token_id, padding_side="left") attention_mask = pad([torch.ones_like(t) for t in prompt_tensors], padding_value=0, padding_side="left") generate_inputs = {"input_ids": padded_ids, "attention_mask": attention_mask} for key, value in multimodal_fields.items(): if isinstance(value, torch.Tensor): generate_inputs[key] = value elif isinstance(value, list) and value and isinstance(value[0], list): generate_inputs[key] = pad([torch.tensor(x) for x in value], padding_value=0, padding_side="left") else: generate_inputs[key] = torch.tensor(np.array(value)) generate_inputs = Trainer._prepare_inputs(self, generate_inputs) gen_config = shallow_copy(self.generation_config) gen_config.output_logits = True gen_config.return_dict_in_generate = True with ( profiling_context(self, "transformers.generate"), unwrap_model_for_generation( self.model_wrapped, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=self.generation_kwargs, ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): gen_output = unwrapped_model.generate( **generate_inputs, generation_config=gen_config, disable_compile=True ) prompt_ids_tensor, prompt_mask = generate_inputs["input_ids"], generate_inputs["attention_mask"] prompt_length = prompt_ids_tensor.size(1) completion_ids = gen_output.sequences[:, prompt_length:] sampled_chunks = [] topk_logps_chunks = [] if needs_topk else None topk_ids_chunks = [] if needs_topk else None for t, logits_t in enumerate(gen_output.logits): # logits_t: (B, V) logits_t = logits_t / self.temperature # exact sampled-token logprob without allocating (B, V) log_softmax output logZ_t = torch.logsumexp(logits_t, dim=-1, keepdim=True) sampled_ids_t = completion_ids[:, t : t + 1] sampled_lp_t = logits_t.gather(-1, sampled_ids_t) - logZ_t sampled_chunks.append(sampled_lp_t.cpu()) if needs_topk: topk_logits_t, topk_ids_t = torch.topk(logits_t, k=K, dim=-1) # (B, K), (B, K) topk_lp_t = topk_logits_t - logZ_t # Ensure sampled token is included in A'_t = TopK ∪ {a_t} missing = ~(topk_ids_t == sampled_ids_t).any(dim=-1) if missing.any(): topk_ids_t = topk_ids_t.clone() topk_lp_t = topk_lp_t.clone() topk_ids_t[missing, -1] = sampled_ids_t[missing, 0] topk_lp_t[missing, -1] = sampled_lp_t[missing, 0] topk_ids_chunks.append(topk_ids_t.cpu()) topk_logps_chunks.append(topk_lp_t.cpu()) # Mask everything after the first EOS token is_eos = completion_ids == self.eos_token_id has_eos = is_eos.any(dim=1) eos_idx = torch.full((is_eos.size(0),), is_eos.size(1), dtype=torch.long, device=device) eos_idx[has_eos] = is_eos.int().argmax(dim=1)[has_eos] sequence_indices = torch.arange(is_eos.size(1), device=device).expand(is_eos.size(0), -1) completion_mask = (sequence_indices <= eos_idx.unsqueeze(1)).int() prompt_mask_cpu = prompt_mask.bool().cpu() completion_mask_cpu = completion_mask.bool().cpu() prompt_ids_out = _strip_padding(prompt_ids_tensor.cpu(), prompt_mask_cpu) completion_ids_out = _strip_padding(completion_ids.cpu(), completion_mask_cpu) logprobs_out = _strip_padding(torch.cat(sampled_chunks, dim=1), completion_mask_cpu) if needs_topk: topk_logprobs = _strip_padding(torch.stack(topk_logps_chunks, dim=1), completion_mask_cpu) topk_token_ids = _strip_padding(torch.stack(topk_ids_chunks, dim=1), completion_mask_cpu) else: topk_logprobs = None topk_token_ids = None return prompt_ids_out, completion_ids_out, logprobs_out, topk_logprobs, topk_token_ids def _tool_call_loop( self, prompts, prompt_ids, completion_ids, completions, logprobs, topk_logprobs, topk_token_ids ): """Tool execution loop that also threads top-K logprob data alongside logprobs. Mirrors GRPOTrainer._tool_call_loop but additionally concatenates topk_logprobs and topk_token_ids the same way logprobs is concatenated: real data for model-generated tokens, zero-padding for tool-result tokens. When topk data is None (binary divergence), behaves identically to the parent. """ K = self.divergence_topk has_topk = topk_logprobs is not None tool_calls = [completion[0].get("tool_calls") for completion in completions] idxs_with_tool = [idx for idx, tool_call in enumerate(tool_calls) if tool_call] tool_calls = [tool_calls[idx] for idx in idxs_with_tool] tool_mask = [[1] * len(ids) for ids in completion_ids] tool_call_count = 0 tool_failure_count = 0 iteration_num = 0 while idxs_with_tool and iteration_num < self.max_tool_calling_iterations: prompt_completion_tools = [prompts[i] for i in idxs_with_tool] for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] tool_call_list = tool_calls[idx] prompt_completion_tool = prompt_completion_tools[idx] sync_tool_dict = self._sync_tool_dicts[idx_with_tool] async_tool_dict = self._async_tool_dicts[idx_with_tool] prompt_completion_tool.append(completions[idx_with_tool][-1]) async_coros = [] tool_call_results = [] for tool_call in tool_call_list: tool_call_count += 1 if tool_call["type"] == "function": function = tool_call["function"] name = function["name"] try: if name in sync_tool_dict: tool_call_results.append((name, sync_tool_dict[name](**function["arguments"]))) elif name in async_tool_dict: async_coros.append((name, async_tool_dict[name](**function["arguments"]))) else: raise ValueError(f"Tool {name} not found.") except Exception as err: tool_failure_count += 1 tool_call_results.append((name, {"error": str(err)})) else: tool_failure_count += 1 name = tool_call.get("name", "unknown") tool_call_results.append((name, {"error": f"Unsupported tool call type: {tool_call['type']}"})) if async_coros: async def _run_async_tools(async_coros): coros = [coro for _, coro in async_coros] results = await asyncio.gather(*coros, return_exceptions=True) return [(name, result) for (name, _), result in zip(async_coros, results, strict=False)] async_results = asyncio.run_coroutine_threadsafe( _run_async_tools(async_coros), self.async_loop ).result() for name, result in async_results: if isinstance(result, Exception): tool_failure_count += 1 tool_call_results.append((name, {"error": str(result)})) else: tool_call_results.append((name, result)) for name, result in tool_call_results: tool_message = {"role": "tool", "name": name, "content": str(result)} prompt_completion_tool.append(tool_message) completions[idx_with_tool].append(tool_message) # Tokenize and filter samples whose length exceeds max allowed length pct_ids = self.processing_class.apply_chat_template( prompt_completion_tools, tools=self.tools, chat_template=self.chat_template, add_generation_prompt=True, tokenize=True, return_dict=False, **self.chat_template_kwargs, ) if self.use_vllm and self.vllm_mode == "colocate": max_model_len = self.vllm_generation.llm.llm_engine.model_config.max_model_len elif not self.use_vllm: max_model_len = self.model.config.max_position_embeddings else: raise NotImplementedError( f"Unsupported mode detected: use_vllm={self.use_vllm}, vllm_mode={self.vllm_mode}" ) overlong = [len(pct) >= max_model_len for pct in pct_ids] for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] if overlong[idx]: prompt_length = len(prompt_ids[idx_with_tool]) ct = pct_ids[idx][prompt_length : prompt_length + self.max_completion_length] completion_ids[idx_with_tool] = ct tool_mask[idx_with_tool] += [1] * (len(ct) - len(tool_mask[idx_with_tool])) if logprobs is not None: logprobs[idx_with_tool] += [0.0] * (len(ct) - len(logprobs[idx_with_tool])) if has_topk: topk_logprobs[idx_with_tool] += [[0.0] * K] * (len(ct) - len(topk_logprobs[idx_with_tool])) topk_token_ids[idx_with_tool] += [[0] * K] * (len(ct) - len(topk_token_ids[idx_with_tool])) idxs_with_tool = [idx for idx, o in zip(idxs_with_tool, overlong, strict=True) if not o] prompt_completion_tools = [pct for pct, o in zip(prompt_completion_tools, overlong, strict=True) if not o] if not idxs_with_tool: break # Generate new completions after tool execution pct_prompt_ids, pct_images, pct_multimodal_fields = self._tokenize_prompts(prompt_completion_tools) ( prompt_completion_tool_ids, post_tool_ids, post_tool_logprobs, post_tool_topk_logprobs, post_tool_topk_token_ids, ) = self._generate_single_turn(pct_prompt_ids, pct_images, pct_multimodal_fields) # Sanity check: chat template must be prefix-preserving for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] pct = prompt_completion_tool_ids[idx] if prompt_ids[idx_with_tool] != pct[: len(prompt_ids[idx_with_tool])]: raise ValueError( "The chat template is not prefix-preserving. Please update it to use a prefix-preserving " "format." ) # Truncate so that pct[len(prompt_ids[idx]):] + post_tool does not exceed max_completion_length for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_len = len(prompt_ids[idx_with_tool]) completion_tool_ids = prompt_completion_tool_ids[idx][prompt_len:] excess_length = len(completion_tool_ids) + len(post_tool_ids[idx]) - self.max_completion_length if excess_length > 0: post_tool_ids[idx] = post_tool_ids[idx][:-excess_length] if logprobs is not None: post_tool_logprobs[idx] = post_tool_logprobs[idx][:-excess_length] if has_topk and post_tool_topk_logprobs is not None: post_tool_topk_logprobs[idx] = post_tool_topk_logprobs[idx][:-excess_length] post_tool_topk_token_ids[idx] = post_tool_topk_token_ids[idx][:-excess_length] excess_length = len(completion_tool_ids) + len(post_tool_ids[idx]) - self.max_completion_length if excess_length > 0: prompt_completion_tool_ids[idx] = prompt_completion_tool_ids[idx][:-excess_length] # Update tool_mask and logprobs: tool result tokens get 0/0.0, post-tool model tokens get 1/real values for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_completion_tool_length = len(prompt_completion_tool_ids[idx]) prompt_length = len(prompt_ids[idx_with_tool]) completion_length = len(completion_ids[idx_with_tool]) post_tool_length = len(post_tool_ids[idx]) tool_length = prompt_completion_tool_length - prompt_length - completion_length tool_mask[idx_with_tool] += [0] * tool_length + [1] * post_tool_length if logprobs is not None: logprobs[idx_with_tool] += [0.0] * tool_length + post_tool_logprobs[idx] if has_topk: topk_pad = [[0.0] * K] * tool_length tid_pad = [[0] * K] * tool_length post_topk_lp = post_tool_topk_logprobs[idx] if post_tool_topk_logprobs is not None else [] post_topk_tid = post_tool_topk_token_ids[idx] if post_tool_topk_token_ids is not None else [] topk_logprobs[idx_with_tool] += topk_pad + post_topk_lp topk_token_ids[idx_with_tool] += tid_pad + post_topk_tid # Update completion_ids with the new completions (after tool execution) for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_length = len(prompt_ids[idx_with_tool]) pct = prompt_completion_tool_ids[idx] completion_ids[idx_with_tool] = pct[prompt_length:] + post_tool_ids[idx] # Decode post-tool completions post_tool_completions = [ parse_response(self.processing_class, ids) if ids else {} for ids in post_tool_ids ] for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] if post_tool_completions[idx]: completions[idx_with_tool].append(post_tool_completions[idx]) # Check for further tool calls tool_calls = [completion.get("tool_calls") for completion in post_tool_completions] idxs_with_tool = [idx for idx, tool_call in zip(idxs_with_tool, tool_calls, strict=True) if tool_call] tool_calls = [tool_call for tool_call in tool_calls if tool_call] iteration_num += 1 return ( tool_mask, completions, completion_ids, logprobs, topk_logprobs, topk_token_ids, tool_call_count, tool_failure_count, ) def _generate(self, prompts: list): """Generate completions, handling tool calls, and thread top-K logprob data through the full pipeline. Returns: 9-tuple of (prompt_ids, completion_ids, tool_mask, completions, total_completion_tokens, logprobs, topk_logprobs, topk_token_ids, extra_fields). """ device = self.accelerator.device mode = "train" if self.model.training else "eval" needs_topk = self.divergence_type in ["topk_tv", "topk_kl"] # Copy the prompts to avoid modifying the original list prompts = copy.deepcopy(prompts) if self.rollout_func is not None: # Keep vLLM weights in sync for custom rollouts that rely on vLLM utilities. if self.use_vllm and self.state.global_step != self._last_loaded_step: with profiling_context(self, "sync_weights"): self.vllm_generation.sync_weights() self._last_loaded_step = self.state.global_step # Pass prompts to rollout_func preserving structured messages. # Chat templating must happen inside rollout_func, at the backend boundary, so that # multimodal content (images, typed content blocks) is not lost before rollout logic runs. output = self.rollout_func(prompts, self) required_keys = {"prompt_ids", "completion_ids", "logprobs"} missing_keys = required_keys - output.keys() if missing_keys: missing_keys_list = sorted(missing_keys) raise ValueError(f"rollout_func must return keys {missing_keys_list} in its output dict.") extra_fields = {k: v for k, v in output.items() if k not in required_keys} prompt_ids = output["prompt_ids"] completion_ids = output["completion_ids"] logprobs = output["logprobs"] topk_logprobs = extra_fields.pop("topk_logprobs", None) topk_token_ids = extra_fields.pop("topk_token_ids", None) if needs_topk and (topk_logprobs is None or topk_token_ids is None): raise ValueError( "rollout_func must return keys ['topk_logprobs', 'topk_token_ids'] when divergence_type is " f"{self.divergence_type!r}." ) else: prompt_ids, images, multimodal_fields = self._tokenize_prompts(prompts) prompt_ids, completion_ids, logprobs, topk_logprobs, topk_token_ids = self._generate_single_turn( prompt_ids, images, multimodal_fields ) extra_fields = {} # Decode completions. It's important to use `parse_response` when possible, because it handles tool calls. if is_conversational({"prompt": prompts[0]}): if ( Version(transformers.__version__) >= Version("5.0.0") # parse_response added in v5 and isinstance(self.processing_class, PreTrainedTokenizerBase) # doesn't work with processors and hasattr(self.processing_class, "response_schema") # attribute not set by default for now and self.processing_class.response_schema is not None # only works if the tokenizer has a schema ): completions = [[parse_response(self.processing_class, ids)] for ids in completion_ids] else: contents = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) completions = [[{"role": "assistant", "content": content}] for content in contents] else: completions = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Extract tool calls from the completions and (possibly) execute them if self.tools: ( tool_mask, completions, completion_ids, logprobs, topk_logprobs, topk_token_ids, tool_call_count, tool_failure_count, ) = self._tool_call_loop( prompts, prompt_ids, completion_ids, completions, logprobs, topk_logprobs, topk_token_ids ) else: tool_mask = extra_fields.pop("env_mask", None) # Get completion length per sequence, used for logging prompt_lengths = torch.tensor([len(ids) for ids in prompt_ids], device=device) if tool_mask is not None: completion_lengths = torch.tensor([sum(mask) for mask in tool_mask], device=device) else: completion_lengths = torch.tensor([len(ids) for ids in completion_ids], device=device) agg_prompt_lengths = self.accelerator.gather(prompt_lengths) agg_completion_lengths = self.accelerator.gather(completion_lengths) total_prompt_tokens = agg_prompt_lengths.sum() total_completion_tokens = agg_completion_lengths.sum() if mode == "train": self.state.num_input_tokens_seen += (total_prompt_tokens + total_completion_tokens).item() self._metrics[mode]["num_tokens"] = [self.state.num_input_tokens_seen] self._metrics[mode]["completions/mean_length"].append(agg_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_length"].append(agg_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_length"].append(agg_completion_lengths.float().max().item()) eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids], device=device) agg_is_truncated = self.accelerator.gather(is_truncated) self._metrics[mode]["completions/clipped_ratio"].append(agg_is_truncated.float().mean().item()) term_completion_lengths = agg_completion_lengths[~agg_is_truncated] if len(term_completion_lengths) == 0: term_completion_lengths = torch.zeros(1, device=device) self._metrics[mode]["completions/mean_terminated_length"].append(term_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_terminated_length"].append(term_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_terminated_length"].append(term_completion_lengths.float().max().item()) if self.tools: agg_tool_call_count = self.accelerator.gather(torch.tensor(tool_call_count, device=device)).sum() tool_call_frequency = (agg_tool_call_count / len(agg_prompt_lengths)).item() self._metrics[mode]["tools/call_frequency"].append(tool_call_frequency) agg_tool_failure_count = self.accelerator.gather(torch.tensor(tool_failure_count, device=device)).sum() failure_frequency = ( (agg_tool_failure_count / agg_tool_call_count).item() if agg_tool_call_count > 0 else 0.0 ) self._metrics[mode]["tools/failure_frequency"].append(failure_frequency) return ( prompt_ids, completion_ids, tool_mask, completions, total_completion_tokens, logprobs, topk_logprobs, topk_token_ids, extra_fields, ) @profiling_decorator def _get_per_token_logps_with_topk( self, model, input_ids, attention_mask, logits_to_keep, topk_token_ids, batch_size=None, compute_entropy=False, pixel_values=None, image_grid_thw=None, num_images=None, pixel_attention_mask=None, image_sizes=None, token_type_ids=None, mm_token_type_ids=None, ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor]: """Compute per-token log-probs, (optionally) entropies, and top-K log-probs in one forward pass. Evaluates the current policy's log-probs at the rollout's top-K token IDs from the same forward pass used for per_token_logps, avoiding an extra model call. Args: topk_token_ids: Rollout policy's top-K token IDs, shape (B, T, K). The current policy's log-probs are evaluated at these positions. Returns: Tuple of (per_token_logps, entropies, current_topk_logps). """ batch_size = batch_size or input_ids.size(0) all_logps = [] all_entropies = [] all_topk_logps = [] for start in range(0, input_ids.size(0), batch_size): end = start + batch_size input_ids_batch = input_ids[start:end] attention_mask_batch = attention_mask[start:end] model_inputs = {"input_ids": input_ids_batch, "attention_mask": attention_mask_batch} if image_grid_thw is not None and pixel_values is not None: rows_per_image = image_grid_thw.prod(dim=-1) rows_per_sample = torch.split(rows_per_image, num_images) rows_per_sample = torch.stack([s.sum() for s in rows_per_sample]) cum_rows = torch.cat([torch.tensor([0], device=rows_per_sample.device), rows_per_sample.cumsum(0)]) row_start, row_end = cum_rows[start].item(), cum_rows[end].item() model_inputs["pixel_values"] = pixel_values[row_start:row_end] cum_imgs = torch.tensor([0] + num_images).cumsum(0) img_start, img_end = cum_imgs[start], cum_imgs[end] model_inputs["image_grid_thw"] = image_grid_thw[img_start:img_end] elif pixel_values is not None: model_inputs["pixel_values"] = pixel_values[start:end] if pixel_attention_mask is not None: model_inputs["pixel_attention_mask"] = pixel_attention_mask[start:end] if image_sizes is not None: model_inputs["image_sizes"] = image_sizes[start:end] if token_type_ids is not None: model_inputs["token_type_ids"] = token_type_ids[start:end] if mm_token_type_ids is not None: model_inputs["mm_token_type_ids"] = mm_token_type_ids[start:end] if "logits_to_keep" in self.model_kwarg_keys: model_inputs["logits_to_keep"] = logits_to_keep + 1 model_inputs["use_cache"] = False logits = model(**model_inputs).logits logits = logits[:, :-1, :] logits = logits[:, -logits_to_keep:, :] logits = logits / self.temperature completion_ids = input_ids_batch[:, -logits_to_keep:] logps = selective_log_softmax(logits, completion_ids) all_logps.append(logps) if compute_entropy: with torch.no_grad(): entropies = entropy_from_logits(logits) all_entropies.append(entropies) with torch.no_grad(): topk_logps = selective_log_softmax(logits, topk_token_ids[start:end]) all_topk_logps.append(topk_logps) logps = torch.cat(all_logps, dim=0) entropies = torch.cat(all_entropies, dim=0) if compute_entropy else None topk_logps = torch.cat(all_topk_logps, dim=0) return logps, entropies, topk_logps def _generate_and_score_completions( self, inputs: list[dict[str, torch.Tensor | Any]] ) -> dict[str, torch.Tensor | Any]: device = self.accelerator.device mode = "train" if self.model.training else "eval" prompts = [x["prompt"] for x in inputs] if self.environments: for prompt, environment, reset_kwargs in zip(prompts, self.environments, inputs, strict=True): observation = environment.reset(**reset_kwargs) if observation is None: continue prompt[-1]["content"] += observation if "images" in inputs[0]: images = [example.get("images") for example in inputs] elif "image" in inputs[0]: images = [[example.get("image")] if example.get("image") is not None else None for example in inputs] else: images = None # Transformers requires at least one image in the batch, otherwise it throws an error if images is not None and all(img_list == [] for img_list in images): images = None # If the prompts are conversational and the inputs contain images, we need to convert the prompts from # [{"role": "user", "content": "What color is the sky?"}] to # [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What color is the sky?"}]}] if images is not None: if not is_conversational(inputs[0]): raise ValueError( "Multimodal training requires conversational prompts. It looks like the dataset contains " "non-conversational inputs, likely because a chat template was applied before passing the dataset " "to the trainer. Please provide the raw conversational prompts and let the trainer apply the chat " "template internally." ) prompts = [ prepare_multimodal_messages(prompt, image_list) for prompt, image_list in zip(prompts, images, strict=True) ] ( prompt_ids_list, completion_ids_list, tool_mask_list, completions, num_items_in_batch, sampling_per_token_logps_list, topk_logprobs_list, topk_token_ids_list, extra_fields, ) = self._generate(prompts) # Convert lists of token IDs to padded tensors prompt_ids = [torch.tensor(ids) for ids in prompt_ids_list] prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] prompt_ids = pad( prompt_ids, padding_value=self.pad_token_id, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) prompt_mask = pad( prompt_mask, padding_value=0, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) completion_ids = [torch.tensor(ids) for ids in completion_ids_list] completion_mask = [torch.ones_like(ids, dtype=torch.long) for ids in completion_ids] completion_ids = pad( completion_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) sampling_per_token_logps = [torch.tensor(logps) for logps in sampling_per_token_logps_list] sampling_per_token_logps = pad( sampling_per_token_logps, padding_value=0.0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) if tool_mask_list is not None: tool_mask = [torch.tensor(mask) for mask in tool_mask_list] tool_mask = pad( tool_mask, padding_value=1, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) else: tool_mask = None if topk_logprobs_list is not None: sampling_topk_logps = [torch.tensor(lp) for lp in topk_logprobs_list] sampling_topk_logps = pad( sampling_topk_logps, padding_value=0.0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) sampling_topk_token_ids = [torch.tensor(tid, dtype=torch.long) for tid in topk_token_ids_list] sampling_topk_token_ids = pad( sampling_topk_token_ids, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) else: sampling_topk_logps = None sampling_topk_token_ids = None # If mask_truncated_completions is enabled, zero out truncated completions for attention and loss masking if self.mask_truncated_completions: eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids_list], device=device) # Mask completion_mask for attention masking completion_mask = completion_mask * (~is_truncated).unsqueeze(1).int() # Also mask tool_mask for consistency in multi-turn training if tool_mask is not None: tool_mask = tool_mask * (~is_truncated).unsqueeze(1).int() # Concatenate prompt_mask with completion_mask for logit computation prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # (B, P+C) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size num_images = [len(img_list) for img_list in images] if images is not None else None # Get forward_kwargs for models with multimodal inputs if images is not None: prompts_text = [ apply_chat_template( {"prompt": prompt}, self.processing_class, tools=self.tools, **self.chat_template_kwargs )["prompt"] for prompt in prompts ] prompt_inputs = self.processing_class(images=images, text=prompts_text, padding=True, return_tensors="pt") prompt_inputs = Trainer._prepare_inputs(self, prompt_inputs) forward_kwargs = {k: v for k, v in prompt_inputs.items() if k not in ["input_ids", "attention_mask"]} else: forward_kwargs = {} # If token_type_ids are used, extend them with zeros for the completion part if "token_type_ids" in forward_kwargs: token_type_ids = forward_kwargs["token_type_ids"] if self.pad_to_multiple_of is not None: padding_size = prompt_ids.size(1) - token_type_ids.size(1) if padding_size > 0: token_type_ids = torch.cat( [token_type_ids.new_zeros((token_type_ids.size(0), padding_size)), token_type_ids], dim=1 ) forward_kwargs["token_type_ids"] = torch.cat( [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # If mm_token_type_ids are used, extend them with zeros for the completion part if "mm_token_type_ids" in forward_kwargs: mm_token_type_ids = forward_kwargs["mm_token_type_ids"] if self.pad_to_multiple_of is not None: padding_size = prompt_ids.size(1) - mm_token_type_ids.size(1) if padding_size > 0: mm_token_type_ids = torch.cat( [mm_token_type_ids.new_zeros((mm_token_type_ids.size(0), padding_size)), mm_token_type_ids], dim=1, ) forward_kwargs["mm_token_type_ids"] = torch.cat( [mm_token_type_ids, mm_token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # When gradient checkpointing is enabled with use_reentrant=True (non default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): # Compute the per-token log probabilities for the reference model if self.beta != 0.0: if self.ref_model is not None: ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.ref_model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: # When training a PEFT adapter, how we obtain the reference depends on the setup: # - New adapter: disabling adapters yields the base model. # - Re-training an existing adapter: an initial copy is loaded under the name "ref". model = self.accelerator.unwrap_model(self.model) with use_adapter(model, adapter_name="ref" if "ref" in model.peft_config else None): ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: ref_per_token_logps = None # Decode prompts_text = self.processing_class.batch_decode(prompt_ids, skip_special_tokens=True) completions_text = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Merge extra_fields from rollout_func into inputs for reward functions if extra_fields: for i, inp in enumerate(inputs): for key, values in extra_fields.items(): if isinstance(values, list) and i < len(values): inp[key] = values[i] elif not isinstance(values, list): inp[key] = values # Calculate rewards for each reward function. rewards_per_func aggregates rewards across all processes. This is # important because rewards will be normalized per group, and completions are distributed. We will later slice # rewards_per_func to extract each process's subset. rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list) num_generations = self.num_generations if mode == "train" else self.num_generations_eval if self.multi_objective_aggregation == "sum_then_normalize": # Apply weights to each reward function's output and sum rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) mean_grouped_rewards = rewards.view(-1, num_generations).mean(dim=1) mean_grouped_rewards = mean_grouped_rewards.repeat_interleave(num_generations, dim=0) if self.scale_rewards in ["group", "none"]: # If self.scale_rewards = "none", we'll only use std_rewards to check for zero std for logging if num_generations > 1: std_rewards = rewards.view(-1, num_generations).std(dim=1) std_rewards = std_rewards.repeat_interleave(num_generations, dim=0) else: # doesn't occur during training, but could occur in eval when num_generations_eval=1 std_rewards = torch.zeros_like(rewards) elif self.scale_rewards == "batch": # Compute global std if rewards.numel() > 1: std_rewards = rewards.std().expand_as(rewards) else: # doesn't occur during training, but could occur in eval when num_generations_eval=batch_size=1 std_rewards = torch.zeros_like(rewards) else: raise ValueError( f"Invalid value for scale_rewards: {self.scale_rewards}. Must be one of 'batch', 'group', or 'none'." ) advantages = rewards - mean_grouped_rewards if self.scale_rewards != "none": advantages = advantages / (std_rewards + 1e-4) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) # for logging elif self.multi_objective_aggregation == "normalize_then_sum": grouped = rewards_per_func.view(-1, num_generations, len(self.reward_funcs)) mean_k = torch.nanmean(grouped, dim=1, keepdim=True) std_k = nanstd(grouped, dim=1, keepdim=True) if num_generations > 1 else torch.zeros_like(mean_k) reward_k = (grouped - mean_k) / (std_k + 1e-4) reward_k = reward_k.view(-1, len(self.reward_funcs)) rewards = (reward_k * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) std_rewards = rewards.std().expand_as(rewards) if rewards.numel() > 1 else torch.zeros_like(rewards) advantages = (rewards - rewards.mean()) / (std_rewards + 1e-4) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) # for logging else: raise ValueError( f"Invalid multi_objective_aggregation: {self.multi_objective_aggregation}. Must be " "'sum_then_normalize' or 'normalize_then_sum'." ) # Slice to keep only the local part of the data process_slice = slice( self.accelerator.process_index * len(prompts), (self.accelerator.process_index + 1) * len(prompts), ) all_process_advantages = advantages.clone() # keep the aggregated advantages for logging advantages = advantages[process_slice] # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) for i, reward_func_name in enumerate(self.reward_func_names): mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) std_func_rewards = nanstd(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_func_rewards) rewards = rewards_per_func.nansum(dim=1) self._metrics[mode]["reward"].append(rewards.mean().item()) self._metrics[mode]["reward_std"].append(rewards.std().item()) self._metrics[mode]["frac_reward_zero_std"].append(is_std_zero.float().mean().item()) # Log prompt and completion texts self._logs["prompt"].extend(gather_object(prompts_text)) self._logs["completion"].extend(gather_object(completions_text)) for i, name in enumerate(self.reward_func_names): self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) self._logs["advantages"].extend(all_process_advantages.tolist()) # Flush user-logged extra columns (from log_extra), gathering across processes. # Keys must be sorted so that all ranks call gather_object in the same order, otherwise values # get mis-attributed across columns (dict insertion order may differ between processes). for column in sorted(self._pending_extra_logs): self._logs["extra"][column].extend(gather_object(self._pending_extra_logs[column])) self._pending_extra_logs.clear() # Flush user-logged metrics (from log_metric), averaging across processes. # Keys must be sorted so that all ranks call accelerator.gather in the same order, otherwise values # get mis-attributed across metrics (dict insertion order may differ between processes). for name in sorted(self._pending_metrics): values = self._pending_metrics[name] local_mean = sum(values) / len(values) global_mean = self.accelerator.gather(torch.tensor(local_mean, device=device)).mean().item() self._metrics[mode][name].append(global_mean) self._pending_metrics.clear() if images is not None: self._logs["images"].extend(gather_object(images)) output = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "advantages": advantages, "num_items_in_batch": num_items_in_batch, "sampling_per_token_logps": sampling_per_token_logps, } if ref_per_token_logps is not None: output["ref_per_token_logps"] = ref_per_token_logps if "pixel_values" in forward_kwargs: output["pixel_values"] = forward_kwargs["pixel_values"] if "image_grid_thw" in forward_kwargs: output["image_grid_thw"] = forward_kwargs["image_grid_thw"] if "pixel_attention_mask" in forward_kwargs: output["pixel_attention_mask"] = forward_kwargs["pixel_attention_mask"] if "image_sizes" in forward_kwargs: output["image_sizes"] = forward_kwargs["image_sizes"] if "token_type_ids" in forward_kwargs: output["token_type_ids"] = forward_kwargs["token_type_ids"] if "mm_token_type_ids" in forward_kwargs: output["mm_token_type_ids"] = forward_kwargs["mm_token_type_ids"] if images is not None: output["num_images"] = num_images if tool_mask is not None: output["tool_mask"] = tool_mask if sampling_topk_logps is not None: output["sampling_topk_logps"] = sampling_topk_logps if sampling_topk_token_ids is not None: output["sampling_topk_token_ids"] = sampling_topk_token_ids return output @torch.no_grad() def _compute_divergence_mask( self, per_token_logps, sampling_per_token_logps, advantages, completion_mask, current_topk_logps=None, sampling_topk_logps=None, ): """ Compute a per-token trust-region mask based on the configured divergence type. Tokens where the policy has diverged too far from the sampling distribution (in a direction that would increase the loss) are masked out. Args: per_token_logps (`torch.Tensor`): Log-probabilities of the current policy at the sampled tokens, shape `(B, T)`. sampling_per_token_logps (`torch.Tensor`): Log-probabilities of the sampling (rollout) policy at the sampled tokens, shape `(B, T)`. advantages (`torch.Tensor`): Per-token or per-sequence advantage estimates, broadcastable to `(B, T)`. completion_mask (`torch.Tensor`): Binary mask of shape `(B, T)` where `1` indicates valid completion tokens and `0` padding. current_topk_logps (`torch.Tensor` or `None`): Log-probabilities of the current policy at the rollout's top-K token IDs, shape `(B, T, K)`. Required when `divergence_type` is `"topk_tv"` or `"topk_kl"`. sampling_topk_logps (`torch.Tensor` or `None`): Log-probabilities of the sampling policy at the rollout's top-K token IDs, shape `(B, T, K)`. Required when `divergence_type` is `"topk_tv"` or `"topk_kl"`. Returns: `torch.Tensor`: Float mask of shape `(B, T)` where `1.0` indicates tokens to keep and `0.0` tokens to mask out. """ prob = torch.exp(per_token_logps) sampling_prob = torch.exp(sampling_per_token_logps) delta_low = self.epsilon_low delta_high = self.epsilon_high if self.divergence_type == "binary_tv": # TV = |π - μ| divergence = (prob - sampling_prob).abs() # Mask tokens where divergence > threshold AND policy moves away from trust region invalid_pos = (divergence > delta_high) & (prob > sampling_prob) invalid_neg = (divergence > delta_low) & (prob < sampling_prob) mask = torch.where(advantages > 0, ~invalid_pos, ~invalid_neg) elif self.divergence_type == "binary_kl": # Bernoulli KL: D = μ log(μ/π) + (1-μ) log((1-μ)/(1-π)) kl = sampling_prob * (sampling_per_token_logps - per_token_logps) + (1 - sampling_prob) * ( torch.log1p(-sampling_prob.clamp(max=1 - 1e-7)) - torch.log1p(-prob.clamp(max=1 - 1e-7)) ) invalid_pos = (kl > delta_high) & (prob > sampling_prob) invalid_neg = (kl > delta_low) & (prob < sampling_prob) mask = torch.where(advantages > 0, ~invalid_pos, ~invalid_neg) elif self.divergence_type in ("topk_tv", "topk_kl"): current_topk_probs = torch.exp(current_topk_logps.float()) rollout_topk_probs = torch.exp(sampling_topk_logps.float()) # Aggregate remaining probability mass outside top-K into a single rest bucket. rollout_rest = (1.0 - rollout_topk_probs.sum(dim=-1)).clamp(min=1e-12) current_rest = (1.0 - current_topk_probs.sum(dim=-1)).clamp(min=1e-12) if self.divergence_type == "topk_tv": topk_tv = (current_topk_probs - rollout_topk_probs).abs().sum(dim=-1) rest_tv = (current_rest - rollout_rest).abs() divergence = (topk_tv + rest_tv) / 2.0 else: topk_kl = (rollout_topk_probs * (sampling_topk_logps - current_topk_logps)).sum(dim=-1) rest_kl = rollout_rest * (rollout_rest.log() - current_rest.log()) divergence = topk_kl + rest_kl invalid_pos = (divergence > delta_high) & (prob > sampling_prob) invalid_neg = (divergence > delta_low) & (prob < sampling_prob) mask = torch.where(advantages > 0, ~invalid_pos, ~invalid_neg) else: raise ValueError(f"Unknown divergence_type: {self.divergence_type}") return mask.float() * completion_mask def _compute_loss(self, model, inputs): # Compute per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) mask = completion_mask if "tool_mask" not in inputs else completion_mask * inputs["tool_mask"] forward_kwargs = { "pixel_values": inputs.get("pixel_values"), "image_grid_thw": inputs.get("image_grid_thw"), "num_images": inputs.get("num_images"), "pixel_attention_mask": inputs.get("pixel_attention_mask"), "image_sizes": inputs.get("image_sizes"), "token_type_ids": inputs.get("token_type_ids"), "mm_token_type_ids": inputs.get("mm_token_type_ids"), } sampling_topk_token_ids = inputs.get("sampling_topk_token_ids") if self.divergence_type.startswith("topk_") and sampling_topk_token_ids is not None: per_token_logps, entropies, current_topk_logps = self._get_per_token_logps_with_topk( model, input_ids, attention_mask, logits_to_keep, topk_token_ids=sampling_topk_token_ids, compute_entropy=True, **forward_kwargs, ) else: per_token_logps, entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, **forward_kwargs, ) current_topk_logps = None sampling_per_token_logps = inputs["sampling_per_token_logps"] sampling_topk_logps = inputs.get("sampling_topk_logps") advantages = inputs["advantages"] if advantages.dim() == 1: advantages = advantages.unsqueeze(1) # DPPO: compute IS ratio (clamped, detached) and divergence mask log_ratio = per_token_logps - sampling_per_token_logps ratio = torch.exp(log_ratio.clamp(max=math.log(self.clip_ratio_c))).detach() divergence_mask = self._compute_divergence_mask( per_token_logps, sampling_per_token_logps, advantages, mask, current_topk_logps=current_topk_logps, sampling_topk_logps=sampling_topk_logps, ) # DPPO loss: -advantages * ratio * mask * log_prob per_token_loss = -advantages * ratio * divergence_mask * per_token_logps # KL divergence with reference model if self.beta != 0.0: ref_per_token_logps = inputs["ref_per_token_logps"] per_token_kl = ( torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1 ) per_token_loss = per_token_loss + self.beta * per_token_kl mode = "train" if self.model.training else "eval" normalizer = inputs["num_items_in_batch"] / self.accelerator.num_processes loss = (per_token_loss * mask).sum() / normalizer # Log metrics completion_token_count = mask.sum().clamp(min=1.0) def masked_batch_mean(x): if x.shape[1] == 1: return x.mean() return (x * mask).sum() / completion_token_count if self.beta != 0.0: mean_kl = masked_batch_mean(per_token_kl) self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).nanmean().item()) mean_entropy = masked_batch_mean(entropies) self._metrics[mode]["entropy"].append(self.accelerator.gather(mean_entropy).nanmean().item()) prob_diff = (torch.exp(per_token_logps) - torch.exp(sampling_per_token_logps)).abs() self._metrics[mode]["prob_diff/mean"].append( self.accelerator.gather(masked_batch_mean(prob_diff)).nanmean().item() ) per_seq_max = prob_diff.masked_fill(mask == 0, float("-inf")).max(dim=1).values per_seq_min = prob_diff.masked_fill(mask == 0, float("inf")).min(dim=1).values self._metrics[mode]["prob_diff/max"].append(self.accelerator.gather(per_seq_max).max().item()) self._metrics[mode]["prob_diff/min"].append(self.accelerator.gather(per_seq_min).min().item()) self._metrics[mode]["advantages/mean"].append(advantages.mean().item()) self._metrics[mode]["advantages/std"].append(advantages.std().item()) # Log divergence mask statistics (analogous to clip_ratio in GRPO) is_masked = (divergence_mask == 0) & (mask > 0) is_masked_pos = is_masked & (advantages > 0) is_masked_neg = is_masked & (advantages < 0) mask_ratio_pos = masked_batch_mean(is_masked_pos.float()) mask_ratio_neg = masked_batch_mean(is_masked_neg.float()) mask_ratio = masked_batch_mean(is_masked.float()) gathered_mask_ratio_neg = self.accelerator.gather(mask_ratio_neg) self._metrics[mode]["mask_ratio/negative_adv_mean"].append(gathered_mask_ratio_neg.nanmean().item()) gathered_mask_ratio_pos = self.accelerator.gather(mask_ratio_pos) self._metrics[mode]["mask_ratio/positive_adv_mean"].append(gathered_mask_ratio_pos.nanmean().item()) gathered_mask_ratio = self.accelerator.gather(mask_ratio) self._metrics[mode]["mask_ratio/overall_mean"].append(gathered_mask_ratio.nanmean().item()) return loss ================================================ FILE: trl/experimental/gfpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .gfpo_config import GFPOConfig from .gfpo_trainer import GFPOTrainer ================================================ FILE: trl/experimental/gfpo/gfpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from ...trainer.grpo_config import GRPOConfig as _GRPOConfig @dataclass class GFPOConfig(_GRPOConfig): num_remains_in_group: int | None = field( default=None, metadata={ "help": "number inputs remains after group filter function, `'num_remains_in_group'` must be >=2 if given." }, ) def __post_init__(self): super().__post_init__() if self.num_remains_in_group is not None and self.num_remains_in_group >= self.num_generations: raise ValueError( f"Number remains in Group {self.num_remains_in_group} must be less than num_generations : {self.num_generations}." ) ================================================ FILE: trl/experimental/gfpo/gfpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from collections.abc import Callable from typing import Any import torch from accelerate.utils import gather_object from ...data_utils import apply_chat_template, is_conversational, prepare_multimodal_messages from ...models.utils import disable_gradient_checkpointing from ...trainer.grpo_trainer import GRPOTrainer as _GRPOTrainer from ...trainer.utils import nanmax, nanmin, nanstd, pad logger = logging.getLogger(__name__) GroupFilterFunc = Callable[[list[list[Any]], list[list[Any]]], list[list[float]]] class GFPOTrainer(_GRPOTrainer): def __init__( self, model, reward_funcs, args=None, train_dataset=None, eval_dataset=None, processing_class=None, reward_processing_classes=None, group_filter_func=None, callbacks=None, optimizers=(None, None), peft_config=None, ): super().__init__( model=model, reward_funcs=reward_funcs, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=reward_processing_classes, callbacks=callbacks, optimizers=optimizers, peft_config=peft_config, ) self.group_filter_func = group_filter_func self.num_remains_in_group = args.num_remains_in_group if self.group_filter_func is None and self.num_remains_in_group is not None: raise ValueError( f"Group filter function must not be None when num_remains_in_group ({self.num_remains_in_group}) is given." ) if self.group_filter_func is not None and self.num_remains_in_group is None: logger.warning("Group filter function is not activated since num_remains_in_group is not set") def _generate_and_score_completions(self, inputs): device = self.accelerator.device mode = "train" if self.model.training else "eval" prompts = [x["prompt"] for x in inputs] if "images" in inputs[0]: images = [example.get("images") for example in inputs] elif "image" in inputs[0]: images = [[example.get("image")] if example.get("image") is not None else None for example in inputs] else: images = None # Transformers requires at least one image in the batch, otherwise it throws an error if images is not None and all(img_list == [] for img_list in images): images = None # If the prompts are conversational and the inputs contain images, we need to convert the prompts from # [{"role": "user", "content": "What color is the sky?"}] to # [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What color is the sky?"}]}] if images is not None: if not is_conversational(inputs[0]): raise ValueError( "Multimodal training requires conversational prompts. It looks like the dataset contains " "non-conversational inputs, likely because a chat template was applied before passing the dataset " "to the trainer. Please provide the raw conversational prompts and let the trainer apply the chat " "template internally." ) prompts = [ prepare_multimodal_messages(prompt, image_list) for prompt, image_list in zip(prompts, images, strict=True) ] prompt_ids_list, completion_ids_list, num_items_in_batch, sampling_per_token_logps_list, extra_fields = ( self._generate(prompts) ) # Convert lists of token IDs to padded tensors prompt_ids = [torch.tensor(ids) for ids in prompt_ids_list] prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] prompt_ids = pad( prompt_ids, padding_value=self.pad_token_id, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) prompt_mask = pad( prompt_mask, padding_value=0, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_ids = [torch.tensor(ids) for ids in completion_ids_list] completion_mask = [torch.ones_like(ids, dtype=torch.long) for ids in completion_ids] completion_ids = pad( completion_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) if sampling_per_token_logps_list is not None: sampling_per_token_logps = [torch.tensor(logps) for logps in sampling_per_token_logps_list] sampling_per_token_logps = pad( sampling_per_token_logps, padding_value=0.0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) else: sampling_per_token_logps = None # If mask_truncated_completions is enabled, zero out truncated completions in completion_mask if self.mask_truncated_completions: eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids_list], device=device) completion_mask = completion_mask * (~is_truncated).unsqueeze(1).int() # Concatenate prompt_mask with completion_mask for logit computation prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # (B, P+C) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size num_images = [len(img_list) for img_list in images] if images is not None else None # Get forward_kwargs for models with multimodal inputs if images is not None: prompts_text = [ apply_chat_template({"prompt": prompt}, self.processing_class)["prompt"] for prompt in prompts ] prompt_inputs = self.processing_class(images=images, text=prompts_text, padding=True, return_tensors="pt") prompt_inputs = super()._prepare_inputs(prompt_inputs) forward_kwargs = {k: v for k, v in prompt_inputs.items() if k not in ["input_ids", "attention_mask"]} else: forward_kwargs = {} # If token_type_ids are used, extend them with zeros for the completion part if "token_type_ids" in forward_kwargs: token_type_ids = forward_kwargs["token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and token_type_ids must have equal len padding_size = prompt_ids.size(1) - token_type_ids.size(1) if padding_size > 0: token_type_ids = torch.cat( [token_type_ids.new_zeros((token_type_ids.size(0), padding_size)), token_type_ids], dim=1 ) forward_kwargs["token_type_ids"] = torch.cat( [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # If mm_token_type_ids are used, extend them with zeros for the completion part if "mm_token_type_ids" in forward_kwargs: mm_token_type_ids = forward_kwargs["mm_token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and mm_token_type_ids must have equal len padding_size = prompt_ids.size(1) - mm_token_type_ids.size(1) if padding_size > 0: mm_token_type_ids = torch.cat( [mm_token_type_ids.new_zeros((mm_token_type_ids.size(0), padding_size)), mm_token_type_ids], dim=1, ) forward_kwargs["mm_token_type_ids"] = torch.cat( [mm_token_type_ids, mm_token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # When gradient checkpointing is enabled with use_reentrant=True (non default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): # If the generation and optimization steps are misaligned—i.e., if generation does not occur at the end of # a full optimizer step (when gradient_accumulation_steps is not a multiple of generate_every)—then the # samples may come from an earlier version of the model. In that case, we need to track old_per_token_logps # for importance sampling. If the steps are aligned, importance sampling isn't necessary and we set # old_per_token_logps to None. # When using vLLM, we always compute old_per_token_logps for importance sampling, it was shown that the # distribution mismatch between vLLM and the training model can be large and harm the training. generate_every = self.args.steps_per_generation * self.num_iterations # generation frequency if self.args.gradient_accumulation_steps % generate_every != 0 or ( self.use_vllm and self.vllm_importance_sampling_correction ): old_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: old_per_token_logps = None # Compute the importance sampling ratio when using vLLM, to correct for potential distribution mismatch if self.use_vllm and self.vllm_importance_sampling_correction: importance_sampling_ratio = torch.exp(old_per_token_logps - sampling_per_token_logps) importance_sampling_ratio = torch.clamp( importance_sampling_ratio, max=self.vllm_importance_sampling_cap ) # Compute the per-token log probabilities for the reference model if self.beta != 0.0: if self.ref_model is not None: ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.ref_model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: with self.accelerator.unwrap_model(self.model).disable_adapter(): ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: ref_per_token_logps = None # Decode prompts_text = self.processing_class.batch_decode(prompt_ids, skip_special_tokens=True) completions_text = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) if is_conversational(inputs[0]): completions = [] for prompt, completion in zip(prompts, completions_text, strict=True): bootstrap = prompt.pop()["content"] if prompt[-1]["role"] == "assistant" else "" if isinstance(bootstrap, list): # for VLM, the format might be [{"type": "text", "text": "..."}] assert len(bootstrap) == 1 and bootstrap[0]["type"] == "text" bootstrap = bootstrap[0]["text"] completions.append([{"role": "assistant", "content": bootstrap + completion}]) else: completions = completions_text # Calculate rewards for each reward function. rewards_per_func aggregates rewards across all processes. This is # important because rewards will be normalized per group, and completions are distributed. We will later slice # rewards_per_func to extract each process's subset. rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list) # Apply weights to each reward function's output and sum rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) num_in_group = self.num_generations num_inputs_in_device = len(prompts) if self.num_remains_in_group is not None and mode == "train": num_in_group = self.num_remains_in_group all_completions = gather_object(completions) group_filter_scores = self.group_filter_func( group_completions=[ all_completions[i : i + 1 * self.num_generations] for i in range(len(all_completions) // self.num_generations) ], group_rewards=rewards.view(-1, self.num_generations).tolist(), ) group_filter_scores = torch.tensor(group_filter_scores, device=device) _, group_local_indices = torch.topk(group_filter_scores, self.num_remains_in_group, dim=-1) group_row_offsets = torch.arange(0, len(all_completions), self.num_generations, device=device).unsqueeze(1) group_global_indices = group_row_offsets + group_local_indices group_global_indices = group_global_indices.flatten() rewards = rewards[group_global_indices].contiguous() rewards_per_func = rewards_per_func[group_global_indices, :].contiguous() num_inputs_in_device = int(len(prompts) / self.num_generations * self.num_remains_in_group) # Compute grouped-wise rewards mean_grouped_rewards = rewards.view(-1, num_in_group).mean(dim=1) # Normalize the rewards to compute the advantages mean_grouped_rewards = mean_grouped_rewards.repeat_interleave(num_in_group, dim=0) advantages = rewards - mean_grouped_rewards if self.scale_rewards in ["group", "none"]: # If self.scale_rewards = "none", we'll still log group level std std_rewards = rewards.view(-1, num_in_group).std(dim=1) std_rewards = std_rewards.repeat_interleave(num_in_group, dim=0) elif self.scale_rewards == "batch": # Compute global std std_rewards = rewards.std().expand_as(rewards) else: raise ValueError( f"Invalid value for scale_rewards: {self.scale_rewards}. Must be one of 'batch', 'group', or 'none'." ) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) if self.scale_rewards != "none": advantages = advantages / (std_rewards + 1e-4) # Slice to keep only the local part of the data process_slice = slice( self.accelerator.process_index * num_inputs_in_device, (self.accelerator.process_index + 1) * num_inputs_in_device, ) all_process_advantages = advantages.clone() # keep the aggregated advantages for logging advantages = advantages[process_slice] if self.num_remains_in_group is not None and mode == "train": local_input_indices_to_keep = group_global_indices[process_slice] - self.accelerator.process_index * len( prompts ) # step is length of prompts prompt_ids = prompt_ids[local_input_indices_to_keep].contiguous() prompt_mask = prompt_mask[local_input_indices_to_keep].contiguous() completion_ids = completion_ids[local_input_indices_to_keep].contiguous() completion_mask = completion_mask[local_input_indices_to_keep].contiguous() attention_mask = attention_mask[local_input_indices_to_keep].contiguous() completion_lengths = completion_mask.sum(1) agg_completion_lengths = self.accelerator.gather(completion_lengths) num_items_in_batch = agg_completion_lengths.sum() if sampling_per_token_logps is not None: sampling_per_token_logps = sampling_per_token_logps[local_input_indices_to_keep].contiguous() if old_per_token_logps is not None: old_per_token_logps = old_per_token_logps[local_input_indices_to_keep].contiguous() if ref_per_token_logps is not None: ref_per_token_logps = ref_per_token_logps[local_input_indices_to_keep].contiguous() if self.use_vllm and self.vllm_importance_sampling_correction: importance_sampling_ratio = importance_sampling_ratio[local_input_indices_to_keep].contiguous() # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) for i, reward_func_name in enumerate(self.reward_func_names): mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) std_func_rewards = nanstd(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_func_rewards) self._metrics[mode]["reward"].append(mean_grouped_rewards.mean().item()) self._metrics[mode]["reward_std"].append(std_rewards.mean().item()) self._metrics[mode]["frac_reward_zero_std"].append(is_std_zero.float().mean().item()) # Log prompt and completion texts all_prompts_text = gather_object(prompts_text) all_completions_text = gather_object(completions_text) all_images = gather_object(images) if images is not None else None if self.num_remains_in_group is not None and mode == "train": group_global_indices_list = group_global_indices.tolist() all_prompts_text = [all_prompts_text[i] for i in group_global_indices_list] all_completions_text = [all_completions_text[i] for i in group_global_indices_list] if images is not None: all_images = [all_images[i] for i in group_global_indices_list] self._logs["prompt"].extend(all_prompts_text) self._logs["completion"].extend(all_completions_text) for i, name in enumerate(self.reward_func_names): self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) self._logs["advantages"].extend(all_process_advantages.tolist()) if images is not None: self._logs["images"].extend(all_images) if self.use_vllm and self.vllm_importance_sampling_correction: delta = torch.abs(old_per_token_logps - sampling_per_token_logps) delta = delta[completion_mask.bool()] mean_delta = torch.mean(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) max_delta = torch.max(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) self._metrics[mode]["sampling/sampling_logp_difference/mean"].append( self.accelerator.gather(mean_delta).mean().item() ) self._metrics[mode]["sampling/sampling_logp_difference/max"].append( self.accelerator.gather(max_delta).max().item() ) flat_is_ratio = importance_sampling_ratio[completion_mask.bool()] min_importance_sampling_ratio = ( torch.min(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) mean_importance_sampling_ratio = ( torch.mean(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) max_importance_sampling_ratio = ( torch.max(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( nanmin(self.accelerator.gather(min_importance_sampling_ratio)).item() ) self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item() ) self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( nanmax(self.accelerator.gather(max_importance_sampling_ratio)).item() ) output = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "advantages": advantages, "num_items_in_batch": num_items_in_batch, } if old_per_token_logps is not None: output["old_per_token_logps"] = old_per_token_logps if self.use_vllm and self.vllm_importance_sampling_correction: output["importance_sampling_ratio"] = importance_sampling_ratio if ref_per_token_logps is not None: output["ref_per_token_logps"] = ref_per_token_logps if "pixel_values" in forward_kwargs: output["pixel_values"] = forward_kwargs["pixel_values"] if "image_grid_thw" in forward_kwargs: output["image_grid_thw"] = forward_kwargs["image_grid_thw"] if "pixel_attention_mask" in forward_kwargs: output["pixel_attention_mask"] = forward_kwargs["pixel_attention_mask"] if "image_sizes" in forward_kwargs: output["image_sizes"] = forward_kwargs["image_sizes"] if "token_type_ids" in forward_kwargs: output["token_type_ids"] = forward_kwargs["token_type_ids"] if images is not None: output["num_images"] = num_images return output ================================================ FILE: trl/experimental/gkd/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .gkd_config import GKDConfig from .gkd_trainer import GKDTrainer __all__ = ["GKDConfig", "GKDTrainer"] ================================================ FILE: trl/experimental/gkd/gkd_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from ...trainer.sft_config import SFTConfig @dataclass class GKDConfig(SFTConfig): """ Configuration class for [`experimental.gkd.GKDTrainer`]. This class includes only the parameters that are specific to GKD training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] and [`SFTConfig`] documentation. Args: temperature (`float`, *optional*, defaults to `0.9`): Temperature for sampling. The higher the temperature, the more random the completions. lmbda (`float`, *optional*, defaults to `0.5`): Lambda parameter that controls the student data fraction (i.e., the proportion of on-policy student-generated outputs). beta (`float`, *optional*, defaults to `0.5`): Interpolation coefficient between `0.0` and `1.0` of the Generalized Jensen-Shannon Divergence loss. When beta is `0.0`, the loss is the KL divergence. When beta is `1.0`, the loss is the Inverse KL Divergence. max_new_tokens (`int`, *optional*, defaults to `128`): Maximum number of tokens to generate per completion. teacher_model_name_or_path (`str`, *optional*): Model name or path of the teacher model. If `None`, the teacher model will be the same as the model being trained. teacher_model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the teacher model from a string. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. seq_kd (`bool`, *optional*, defaults to `False`): Seq_kd parameter that controls whether to perform Sequence-Level KD (can be viewed as supervised FT on teacher-generated output). """ _VALID_DICT_FIELDS = SFTConfig._VALID_DICT_FIELDS + ["teacher_model_init_kwargs"] temperature: float = field( default=0.9, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) lmbda: float = field( default=0.5, metadata={ "help": "Lambda parameter that controls the student data fraction (i.e., the proportion of on-policy " "student-generated outputs)." }, ) beta: float = field( default=0.5, metadata={ "help": "Interpolation coefficient between `0.0` and `1.0` of the Generalized Jensen-Shannon Divergence " "loss. When beta is `0.0`, the loss is the KL divergence. When beta is `1.0`, the loss is the Inverse KL " "Divergence." }, ) max_new_tokens: int = field( default=128, metadata={"help": "Maximum number of tokens to generate per completion."}, ) teacher_model_name_or_path: str | None = field( default=None, metadata={ "help": "Model name or path of the teacher model. If `None`, the teacher model will be the same as the " "model being trained." }, ) teacher_model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the " "teacher model from a string." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropouts in `model`."}, ) seq_kd: bool = field( default=False, metadata={ "help": "Seq_kd parameter that controls whether to perform Sequence-Level KD (can be viewed as supervised " "FT on teacher-generated output)." }, ) def __post_init__(self): super().__post_init__() # check lmbda and beta are in the range [0, 1] if self.lmbda < 0.0 or self.lmbda > 1.0: raise ValueError("lmbda must be in the range [0.0, 1.0].") if self.beta < 0.0 or self.beta > 1.0: raise ValueError("beta must be in the range [0.0, 1.0].") ================================================ FILE: trl/experimental/gkd/gkd_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import random import textwrap from collections.abc import Callable from typing import Any import torch import torch.nn as nn import torch.nn.functional as F from datasets import Dataset from transformers import ( AutoModelForCausalLM, BaseImageProcessor, DataCollator, FeatureExtractionMixin, GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, ) from transformers.trainer_utils import EvalPrediction from transformers.utils import is_liger_kernel_available, is_peft_available from ...models import prepare_deepspeed from ...models.utils import unwrap_model_for_generation from ...trainer.sft_trainer import SFTTrainer from ...trainer.utils import disable_dropout_in_model from ..utils import DataCollatorForChatML, empty_cache from .gkd_config import GKDConfig if is_peft_available(): from peft import PeftConfig if is_liger_kernel_available(): from liger_kernel.chunked_loss import LigerFusedLinearJSDLoss class GKDTrainer(SFTTrainer): """Trainer for Generalized Knowledge Distillation (GKD) of language models. For details on GKD, see the paper: [On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes](https://huggingface.co/papers/2306.13649). Args: model ([`~transformers.PreTrainedModel`] or `torch.nn.Module` or `str`, *optional*): Model to be trained, or the string identifier of the model to be instantiated from a pretrained model. teacher_model ([`~transformers.PreTrainedModel`] or `torch.nn.Module` or `str`, *optional*): Teacher model for knowledge distillation, or the string identifier of the model to be instantiated from a pretrained model. args ([`experimental.gkd.GKDConfig`], *optional*): Training arguments. data_collator ([`~transformers.DataCollator`], *optional*): Data collator to batch samples from the dataset. It defaults to a [`experimental.utils.DataCollatorForChatML`] using the `processing_class`. train_dataset ([`~datasets.Dataset`], *optional*): Dataset for training. eval_dataset ([`~datasets.Dataset`] or `dict` of [`~datasets.Dataset`], *optional*): Dataset for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Class to process the data. compute_metrics (`Callable`, *optional*): Function to compute metrics at evaluation. Must take in an [`~transformers.EvalPrediction`] and return a dictionary string to float. callbacks (`list` of [`~transformers.TrainerCallback`], *optional*): Callbacks to use during training. optimizers (`tuple` of `torch.optim.Optimizer` and `torch.optim.lr_scheduler.LambdaLR`, *optional*, defaults to `(None, None)`): Tuple containing the optimizer and the learning rate scheduler to use for training. preprocess_logits_for_metrics (`Callable`, *optional*): Function to preprocess the logits before computing the metrics. Must take in the `logits` and `labels` and return the logits to be used for metrics computation. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration to use PEFT for training. If `None`, PEFT is not used. If provided, the `model` will be wrapped with the specified PEFT adapter. formatting_func (`Callable`, *optional*): Function to format the dataset. Must take in an example and return an example. """ _tag_names = ["trl", "gkd"] _name = "GKD" _paper = { "title": "On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes", "id": "2306.13649", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{agarwal2024on-policy, title = {{On-Policy Distillation of Language Models: Learning from Self-Generated Mistakes}}, author = {Rishabh Agarwal and Nino Vieillard and Yongchao Zhou and Piotr Stanczyk and Sabela Ramos Garea and Matthieu Geist and Olivier Bachem}, year = 2024, booktitle = {The Twelfth International Conference on Learning Representations, {ICLR} 2024, Vienna, Austria, May 7-11, 2024}, publisher = {OpenReview.net}, url = {https://openreview.net/forum?id=3zKtaqxLhW}, }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str | None = None, teacher_model: PreTrainedModel | nn.Module | str = None, args: GKDConfig | None = None, data_collator: DataCollator | None = None, # type: ignore train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: "PeftConfig | None" = None, formatting_func: Callable | None = None, ): # Ensure Trainer does not drop non-signature columns used by the collator (e.g., "prompts") args.remove_unused_columns = False # Respect a user-provided data_collator; otherwise, provide a ChatML collator that if data_collator is None: data_collator = DataCollatorForChatML(tokenizer=processing_class, max_length=args.max_length) # Ensure SFTTrainer does not pre-process the dataset when using a ChatML collator, # so that raw conversational fields (e.g., "messages") remain available to the collator. if args.dataset_kwargs is None: args.dataset_kwargs = {"skip_prepare_dataset": True} else: args.dataset_kwargs["skip_prepare_dataset"] = True # Liger fused GKD loss (JSD) self.use_liger_gkd_loss = False if args.use_liger_kernel: self.liger_jsd_loss = LigerFusedLinearJSDLoss( beta=args.beta, ignore_index=-100, temperature=args.temperature, compiled=False, ) self.use_liger_gkd_loss = True super().__init__( model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, peft_config=peft_config, formatting_func=formatting_func, ) if args.teacher_model_init_kwargs is None: teacher_model_init_kwargs = {} elif not isinstance(teacher_model, str): raise ValueError( "You passed teacher_model_init_kwargs to the GKDConfig, but your teacher_model is already instantiated." ) else: teacher_model_init_kwargs = args.teacher_model_init_kwargs teacher_model_init_kwargs["dtype"] = ( teacher_model_init_kwargs["dtype"] if teacher_model_init_kwargs["dtype"] in ["auto", None] else getattr(torch, teacher_model_init_kwargs["dtype"]) ) if isinstance(teacher_model, str): teacher_model = AutoModelForCausalLM.from_pretrained(teacher_model, **teacher_model_init_kwargs) # Disable dropout in the model if args.disable_dropout: disable_dropout_in_model(self.model) if self.is_deepspeed_enabled: self.teacher_model = prepare_deepspeed(teacher_model, self.accelerator) else: self.teacher_model = self.accelerator.prepare_model(teacher_model, evaluation_mode=True) self.lmbda = args.lmbda self.beta = args.beta self.temperature = args.temperature self.seq_kd = args.seq_kd generation_kwargs = { "max_new_tokens": args.max_new_tokens, "temperature": args.temperature, "do_sample": True, "top_k": 0, "use_cache": False if args.gradient_checkpointing else True, "pad_token_id": self.processing_class.pad_token_id, } self.generation_config = GenerationConfig(**generation_kwargs) # Keep training-specific generation kwargs to overwrite model's original generation config self.generation_kwargs = generation_kwargs # Set custom EOS tokens if they are specified by the model's generation # config. This is important for models with the Llama 3 chat template, # which use special tokens <|eot_id|> and <|eom_id|> to mark the end of # turns or messages. if ( hasattr(self.model.generation_config, "eos_token_id") and self.model.generation_config.eos_token_id is not None ): self.generation_config.eos_token_id = self.model.generation_config.eos_token_id @staticmethod def generalized_jsd_loss( student_logits, teacher_logits, labels=None, beta=0.5, temperature=1.0, reduction="batchmean" ): """ Compute the generalized Jensen-Shannon Divergence loss for knowledge distillation using F.kl_div. See Eq. (1) of https://huggingface.co/papers/2306.13649 for the definition. Args: student_logits: Tensor of shape (batch_size, sequence_length, vocab_size) teacher_logits: Tensor of shape (batch_size, sequence_length, vocab_size) labels: Tensor of shape (batch_size, sequence_length) with -100 for padding tokens to ignore when computing loss beta: Interpolation coefficient between 0 and 1 (default: 0.5) temperature: Softmax temperature (default: 1.0) reduction: Specifies the reduction to apply to the output (default: 'batchmean') Returns: loss: Scalar tensor with the generalized JSD loss """ # Apply temperature scaling student_logits = student_logits / temperature teacher_logits = teacher_logits / temperature # Compute log probabilities for student and probabilities for teacher student_log_probs = F.log_softmax(student_logits, dim=-1) teacher_log_probs = F.log_softmax(teacher_logits, dim=-1) if beta == 0: jsd = F.kl_div(student_log_probs, teacher_log_probs, reduction="none", log_target=True) elif beta == 1: jsd = F.kl_div(teacher_log_probs, student_log_probs, reduction="none", log_target=True) else: # Compute the log of the mixture distribution # log(a + b) = log(exp(log(a)) + exp(log(b))) -> for mixture beta = torch.tensor(beta, dtype=student_log_probs.dtype, device=student_log_probs.device) mixture_log_probs = torch.logsumexp( torch.stack([student_log_probs + torch.log1p(-beta), teacher_log_probs + torch.log(beta)]), dim=0, ) # Compute KL divergences using F.kl_div # PyTorch differs from the standard mathematical definition, so the order of the probability distributions is swapped compared to that defined in the paper. kl_teacher = F.kl_div(mixture_log_probs, teacher_log_probs, reduction="none", log_target=True) kl_student = F.kl_div(mixture_log_probs, student_log_probs, reduction="none", log_target=True) # Compute the Generalized Jensen-Shannon Divergence jsd = beta * kl_teacher + (1 - beta) * kl_student # Masking if labels is not None: mask = labels != -100 jsd = jsd[mask] # Apply reduction if reduction == "batchmean": return jsd.sum() / mask.sum() if labels is not None else jsd.sum() / jsd.size(0) elif reduction == "sum": return jsd.sum() elif reduction == "mean": return jsd.mean() else: return jsd def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): if self.use_liger_gkd_loss: # Forward only through the base models (avoid lm_head to save memory) unwrapped_student = self.accelerator.unwrap_model(model) if hasattr(unwrapped_student, "get_decoder") and unwrapped_student.get_decoder() is not None: base_student = unwrapped_student.get_decoder() else: base_student = getattr( unwrapped_student, getattr(unwrapped_student, "base_model_prefix", "model"), unwrapped_student ) student_outputs = base_student( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], use_cache=False, ) self.teacher_model.eval() unwrapped_teacher = self.accelerator.unwrap_model(self.teacher_model) if hasattr(unwrapped_teacher, "get_decoder") and unwrapped_teacher.get_decoder() is not None: base_teacher = unwrapped_teacher.get_decoder() else: base_teacher = getattr( unwrapped_teacher, getattr(unwrapped_teacher, "base_model_prefix", "model"), unwrapped_teacher ) with torch.no_grad(): teacher_outputs = base_teacher( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], use_cache=False, ) # hidden states (shifted) student_hidden = student_outputs.last_hidden_state[:, :-1] teacher_hidden = teacher_outputs.last_hidden_state[:, :-1] # Release full outputs to free memory del student_outputs, teacher_outputs # labels mask and labels (shifted) labels_mask = inputs["labels"] != -100 masked_input_ids = torch.where( labels_mask, inputs["input_ids"], torch.full_like(inputs["input_ids"], -100) ) true_labels = masked_input_ids[:, 1:].contiguous() # Release intermediate tensors del labels_mask, masked_input_ids # heads student_head = unwrapped_student.get_output_embeddings() teacher_head = unwrapped_teacher.get_output_embeddings() # liger fused jsd loss loss = self.liger_jsd_loss( student_input=student_hidden, student_weight=student_head.weight, teacher_input=teacher_hidden, teacher_weight=teacher_head.weight, true_labels=true_labels, student_bias=getattr(student_head, "bias", None), teacher_bias=getattr(teacher_head, "bias", None), ) # Release hidden states after loss computation del student_hidden, teacher_hidden, true_labels else: # compute student output student_outputs = model( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], ) # compute teacher output in eval mode self.teacher_model.eval() with torch.no_grad(): teacher_outputs = self.teacher_model( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], ) # slice the logits for the generated tokens using the inputs["prompts"] lengths prompt_lengths = inputs["prompts"].shape[1] shifted_student_logits = student_outputs.logits[:, prompt_lengths - 1 : -1, :] shifted_teacher_logits = teacher_outputs.logits[:, prompt_lengths - 1 : -1, :] shifted_labels = inputs["labels"][:, prompt_lengths:] # compute loss loss = self.generalized_jsd_loss( student_logits=shifted_student_logits, teacher_logits=shifted_teacher_logits, labels=shifted_labels, beta=self.beta, ) # empty cache empty_cache() # Return loss return (loss, student_outputs) if return_outputs else loss @staticmethod def generate_on_policy_outputs(model, inputs, generation_config, pad_token_id=None): # Generate output with respect to the prompt-only generated_outputs = model.generate( input_ids=inputs["prompts"], attention_mask=inputs.get("prompt_attention_mask", None), generation_config=generation_config, return_dict_in_generate=True, ) # Get the generated token IDs generated_tokens = generated_outputs.sequences # Calculate new attention mask new_attention_mask = torch.ones_like(generated_tokens) new_labels = generated_tokens.clone() # If there's pad_token_id, set attention mask to 0 for padding tokens if pad_token_id is not None: new_labels[new_labels == pad_token_id] = -100 new_attention_mask[generated_tokens == pad_token_id] = 0 return generated_tokens, new_attention_mask, new_labels def training_step( self, model: nn.Module, inputs: dict[str, torch.Tensor | Any], num_items_in_batch: int | None = None ) -> torch.Tensor: """ Perform a training step for the Generalized Knowledge Distillation (GKD) model. This method implements the on-policy learning approach described in the GKD paper. With probability `self.lmbda`, it generates new responses using the student model, which are then used for training instead of the original inputs. """ if self.seq_kd: with ( unwrap_model_for_generation( self.teacher_model, self.accelerator, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model ): new_input_ids, new_attention_mask, new_labels = self.generate_on_policy_outputs( unwrapped_model, inputs, self.generation_config, self.processing_class.pad_token_id ) inputs["input_ids"] = new_input_ids inputs["attention_mask"] = new_attention_mask inputs["labels"] = new_labels if random.random() <= self.lmbda: with ( unwrap_model_for_generation( model, self.accelerator, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model ): new_input_ids, new_attention_mask, new_labels = self.generate_on_policy_outputs( unwrapped_model, inputs, self.generation_config, self.processing_class.pad_token_id ) inputs["input_ids"] = new_input_ids inputs["attention_mask"] = new_attention_mask inputs["labels"] = new_labels loss = super().training_step(model, inputs, num_items_in_batch) return loss ================================================ FILE: trl/experimental/gold/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .gold_config import GOLDConfig from .gold_trainer import GOLDTrainer __all__ = ["GOLDConfig", "GOLDTrainer"] ================================================ FILE: trl/experimental/gold/gold.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl @ git+https://github.com/huggingface/trl.git", # "peft", # "trackio", # ] # /// # docstyle-ignore """ # Full training: python trl/experimental/gold/gold.py \ --model_name_or_path meta-llama/Llama-3.2-1B-Instruct \ --teacher_model_name_or_path Qwen/Qwen2-1.5B-Instruct \ --dataset_name trl-lib/chatbot_arena_completions \ --learning_rate 2e-5 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --output_dir gold-model \ --num_train_epochs 1 \ --push_to_hub # LoRA: python trl/experimental/gold/gold.py \ --model_name_or_path meta-llama/Llama-3.2-1B-Instruct \ --teacher_model_name_or_path Qwen/Qwen2-1.5B-Instruct \ --dataset_name trl-lib/chatbot_arena_completions \ --learning_rate 2e-4 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 8 \ --output_dir gold-model \ --num_train_epochs 1 \ --push_to_hub \ --use_peft \ --lora_r 64 \ --lora_alpha 16 """ import logging from datasets import load_dataset from transformers import AutoTokenizer, GenerationConfig from trl import ( LogCompletionsCallback, ModelConfig, ScriptArguments, TrlParser, get_kbit_device_map, get_peft_config, get_quantization_config, ) from trl.experimental.gold.gold_config import GOLDConfig from trl.experimental.gold.gold_trainer import GOLDTrainer logger = logging.getLogger(__name__) if __name__ == "__main__": parser = TrlParser((ScriptArguments, GOLDConfig, ModelConfig)) script_args, training_args, model_args = parser.parse_args_and_config() ################ # Model & Tokenizer ################ quantization_config = get_quantization_config(model_args) model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, torch_dtype=model_args.dtype, use_cache=False if training_args.gradient_checkpointing else True, device_map=get_kbit_device_map() if quantization_config is not None else None, quantization_config=quantization_config, ) training_args.model_init_kwargs = model_kwargs if training_args.teacher_tokenizer_name_or_path is None and training_args.use_uld_loss: training_args.teacher_tokenizer_name_or_path = training_args.teacher_model_name_or_path teacher_model_kwargs = dict( revision=training_args.teacher_model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, torch_dtype=model_args.dtype, use_cache=True, device_map=get_kbit_device_map() if quantization_config is not None else None, quantization_config=quantization_config, ) if training_args.teacher_model_init_kwargs is not None: teacher_model_kwargs.update(training_args.teacher_model_init_kwargs) training_args.teacher_model_init_kwargs = teacher_model_kwargs tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token ################ # Dataset ################ dataset = load_dataset(script_args.dataset_name, name=script_args.dataset_config) ################ # Training ################ eval_dataset = None if training_args.eval_strategy != "no": if script_args.dataset_test_split in dataset: eval_dataset = dataset[script_args.dataset_test_split] elif "validation" in dataset: eval_dataset = dataset["validation"] elif "dev" in dataset: eval_dataset = dataset["dev"] trainer = GOLDTrainer( model=model_args.model_name_or_path, teacher_model=training_args.teacher_model_name_or_path, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=eval_dataset, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) if training_args.eval_strategy != "no": generation_config = GenerationConfig( max_new_tokens=training_args.max_completion_length, do_sample=True, temperature=training_args.temperature ) completions_callback = LogCompletionsCallback(trainer, generation_config, num_prompts=8) trainer.add_callback(completions_callback) trainer.train() # Save and push to hub trainer.save_model(training_args.output_dir) if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) ================================================ FILE: trl/experimental/gold/gold_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from dataclasses import dataclass, field from typing import Any from ...trainer.sft_config import SFTConfig @dataclass class GOLDConfig(SFTConfig): r""" Configuration class for [`GOLDTrainer`]. This class includes only the parameters that are specific to GOLD training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] and [`SFTConfig`] documentation. Args: temperature (`float`, *optional*, defaults to `0.9`): Temperature for sampling. The higher the temperature, the more random the completions. lmbda (`float`, *optional*, defaults to `0.5`): Lambda parameter that controls the student data fraction (i.e., the proportion of on-policy student-generated outputs). beta (`float`, *optional*, defaults to `0.5`): Interpolation coefficient between `0.0` and `1.0` of the Generalized Jensen-Shannon Divergence loss. When beta is `0.0`, the loss is the KL divergence. When beta is `1.0`, the loss is the Inverse KL Divergence. max_completion_length (`int`, *optional*, defaults to `128`): Maximum number of tokens to generate per completion. teacher_model_name_or_path (`str`, *optional*): Model name or path of the teacher model. If `None`, the teacher model will be the same as the model being trained. teacher_model_revision (`str` or `None`, *optional*, defaults to `None`): Model revision of the teacher model (e.g., branch name, tag, or commit hash). If `None`, the default revision is used. teacher_model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the teacher model from a string. teacher_tokenizer_name_or_path (`str`, *optional*): Tokenizer name or path for the teacher model. If None when using ULD loss, will use the same tokenizer as the student model (not recommended for cross-tokenizer distillation). disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. seq_kd (`bool`, *optional*, defaults to `False`): Seq_kd parameter that controls whether to perform Sequence-Level KD (can be viewed as supervised FT on teacher-generated output). num_generations (`int`, *optional*, defaults to `1`): Number of generations per prompt. Each prompt is repeated this many times in the generation batch. generation_batch_size (`int` or `None`, *optional*, defaults to `None`): Number of unique prompts per worker per optimizer step. If `None`, it is computed from `(per_device_train_batch_size * gradient_accumulation_steps) // num_generations`. use_uld_loss (`bool`, *optional*, defaults to `False`): Whether to use Universal Logit Distillation (ULD) loss instead of Generalized Jensen-Shannon Divergence loss. uld_crossentropy_weight (`float`, *optional*, defaults to `0.0`): Weight for the cross-entropy loss component in ULD loss. If 0, only ULD distillation loss is used. uld_distillation_weight (`float`, *optional*, defaults to `1.0`): Weight for the distillation loss component in ULD loss. uld_student_temperature (`float`, *optional*, defaults to `1.0`): Temperature for student logits in ULD loss computation. uld_teacher_temperature (`float`, *optional*, defaults to `1.0`): Temperature for teacher logits in ULD loss computation. uld_skip_student_eos (`bool`, *optional*, defaults to `True`): Whether to skip EOS token for student in ULD loss computation. uld_skip_teacher_eos (`bool`, *optional*, defaults to `True`): Whether to skip EOS token for teacher in ULD loss computation. use_vllm (`bool`, *optional*, defaults to `False`): Whether to use vLLM for generating completions from the student model. Requires `vllm` to be installed. vllm_mode (`str`, *optional*, defaults to `"colocate"`): Mode for student vLLM integration. Either `"server"` (connect to a running TRL vLLM server) or `"colocate"` (run vLLM in the same process). vllm_server_host (`str`, *optional*, defaults to `"0.0.0.0"`): Host of the vLLM server for the student model (if `vllm_mode="server"`). vllm_server_port (`int`, *optional*, defaults to `8001`): Port of the vLLM server for the student model (if `vllm_mode="server"`). vllm_server_timeout (`float`, *optional*, defaults to `240.0`): Timeout for connecting to the student vLLM server (if `vllm_mode="server"`). vllm_gpu_memory_utilization (`float`, *optional*, defaults to `0.9`): GPU memory utilization for the colocated student vLLM engine (if `vllm_mode="colocate"`). It is recommended to set this to a low value if the student and teacher models share the same GPU. vllm_tensor_parallel_size (`int`, *optional*, defaults to `1`): Tensor parallel size for the colocated student vLLM engine (if `vllm_mode="colocate"`). vllm_structured_outputs_regex (`str`, *optional*): Regex for vLLM structured outputs for the student model. vllm_sync_frequency (`int`, *optional*, defaults to `1`): Frequency (in training steps) to synchronize student model weights to vLLM engine. Set to 1 to sync after every step. vllm_enable_sleep_mode (`bool`, *optional*, defaults to `False`): Enable vLLM sleep mode to offload student weights/cache during the optimizer step. Keeps GPU memory usage low, but waking the engine adds host–device transfer latency. """ _VALID_DICT_FIELDS = SFTConfig._VALID_DICT_FIELDS + ["teacher_model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-7, metadata={"help": "The initial learning rate for AdamW."}, ) # GOLD-specific parameters temperature: float = field( default=0.9, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) top_p: float = field( default=0.95, metadata={ "help": "If set to float < 1, only the smallest set of most probable tokens with probabilities that add up to " "`top_p` or higher are kept for generation." }, ) top_k: int = field( default=0, metadata={ "help": "Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, " "top-k-filtering is disabled and all tokens are considered." }, ) lmbda: float = field( default=0.5, metadata={ "help": "Lambda parameter that controls the student data fraction (i.e., the proportion of on-policy " "student-generated outputs)." }, ) beta: float = field( default=0.5, metadata={ "help": "Interpolation coefficient between `0.0` and `1.0` of the Generalized Jensen-Shannon Divergence " "loss. When beta is `0.0`, the loss is the KL divergence. When beta is `1.0`, the loss is the Inverse KL " "Divergence." }, ) max_completion_length: int = field( default=128, metadata={"help": "Maximum number of tokens to generate per completion."}, ) teacher_model_name_or_path: str | None = field( default=None, metadata={ "help": "Model name or path of the teacher model. If `None`, the teacher model will be the same as the " "model being trained." }, ) teacher_model_revision: str | None = field( default=None, metadata={ "help": "Model revision of the teacher model (e.g., branch name, tag, or commit hash). If `None`, the " "default revision is used." }, ) teacher_model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the " "teacher model from a string." }, ) teacher_tokenizer_name_or_path: str | None = field( default=None, metadata={ "help": "Tokenizer name or path for the teacher model. If None when using ULD loss, will use the same " "tokenizer as the student model (not recommended for cross-tokenizer distillation)." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropouts in `model`."}, ) seq_kd: bool = field( default=False, metadata={ "help": "Seq_kd parameter that controls whether to perform Sequence-Level KD (can be viewed as supervised " "FT on teacher-generated output)." }, ) num_generations: int = field( default=1, metadata={ "help": "Number of generations per prompt. Increasing this will decrease the number of unique prompts per optimization step." }, ) generation_batch_size: int | None = field( default=None, metadata={ "help": "Number of unique prompts per worker per optimizer step. " "If None, computed from (per_device_train_batch_size * gradient_accumulation_steps) // num_generations." }, ) # ULD Loss parameters use_uld_loss: bool = field( default=False, metadata={ "help": "Whether to use Universal Logit Distillation (ULD) loss instead of Generalized Jensen-Shannon Divergence loss." }, ) use_extended_uld: bool = field( default=True, metadata={ "help": ( "Whether to enable extended ULD alignment that uses tokenizers to align and merge token " "probabilities across student and teacher tokenizations. When True, the trainer will compute " "token mappings and merge probabilities for split tokens; when False, ULD will use simple " "positional truncation like in the original ULD paper." ) }, ) uld_use_hybrid_loss: bool = field( default=False, metadata={ "help": ( "Whether to use a hybrid loss that combines ULD loss and JSD loss. When True, the final loss is a " "a combination of JSD for known token mappings and ULD for unknown token mappings." ) }, ) uld_hybrid_matched_weight: float | None = field( default=None, metadata={ "help": ( "Weight for the matched token loss component when using hybrid ULD + JSD loss. This weight scales " "the JSD loss computed over tokens that have a direct mapping between student and teacher " "tokenizations. If None, uses adaptive weighting based on vocabulary overlap. Must be set together " "with uld_hybrid_unmatched_weight (both None or both float)." ) }, ) uld_hybrid_unmatched_weight: float | None = field( default=None, metadata={ "help": ( "Weight for the unmatched token loss component when using hybrid ULD + JSD loss. This weight scales " "the ULD loss computed over tokens that do not have a direct mapping between student and teacher " "tokenizations. If None, uses adaptive weighting based on vocabulary overlap. Must be set together " "with uld_hybrid_matched_weight (both None or both float)." ) }, ) uld_crossentropy_weight: float = field( default=0.0, metadata={"help": "Weight for the cross-entropy loss component in ULD loss."}, ) uld_distillation_weight: float = field( default=1.0, metadata={"help": "Weight for the distillation loss component in ULD loss."}, ) uld_student_temperature: float = field( default=1.0, metadata={"help": "Temperature for student logits in ULD loss computation."}, ) uld_teacher_temperature: float = field( default=1.0, metadata={"help": "Temperature for teacher logits in ULD loss computation."}, ) uld_skip_student_eos: bool = field( default=True, metadata={"help": "Whether to skip EOS token for student in ULD loss computation."}, ) uld_skip_teacher_eos: bool = field( default=True, metadata={"help": "Whether to skip EOS token for teacher in ULD loss computation."}, ) # transformers paged attention use_transformers_paged: bool = field( default=False, metadata={ "help": "Whether to use the `transformers` paged implementation for generation. If set to `True`, the " "`transformers` paged implementation will be used for generation instead of the default padded " "implementation." }, ) # vLLM parameters use_vllm: bool = field( default=False, metadata={"help": "Whether to use vLLM for generating completions. Requires `vllm` to be installed."}, ) vllm_mode: str = field( default="colocate", metadata={ "help": 'Mode for vLLM integration. Either "server" (connect to a running TRL vLLM server) or "colocate" (run vLLM in the same process).' }, ) vllm_server_host: str = field( default="0.0.0.0", metadata={"help": 'Host of the vLLM server when `vllm_mode="server"`.'}, ) vllm_server_port: int = field( default=8001, metadata={"help": 'Port of the vLLM server when `vllm_mode="server"`.'}, ) vllm_server_timeout: float = field( default=240.0, metadata={"help": 'Timeout (in seconds) for connecting to the vLLM server when `vllm_mode="server"`.'}, ) vllm_gpu_memory_utilization: float = field( default=0.9, metadata={ "help": 'GPU memory utilization for the colocated vLLM engine when `vllm_mode="colocate"`. Lower values reduce contention when sharing a device with the student/teacher models.' }, ) vllm_tensor_parallel_size: int = field( default=1, metadata={"help": 'Tensor parallel size for the colocated vLLM engine when `vllm_mode="colocate"`.'}, ) vllm_structured_outputs_regex: str | None = field( default=None, metadata={"help": "Regex pattern used for vLLM structured outputs (optional)."}, ) vllm_sync_frequency: int = field( default=1, metadata={ "help": "Frequency (in training steps) to synchronize model weights to the vLLM engine. Set to 1 to sync after every step." }, ) vllm_enable_sleep_mode: bool = field( default=False, metadata={ "help": "Enable vLLM sleep mode to offload student weights/cache during the optimizer step. Keeps GPU " "memory usage low, but waking the engine adds host–device transfer latency." }, ) # Parameters that control the logging log_completions: bool = field( default=False, metadata={ "help": "Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is " "installed, it prints the sample. If `wandb` logging is enabled, it logs it to `wandb`." }, ) log_completions_steps: int = field( default=100, metadata={ "help": "Number of steps between logging (prompt, completion) pairs. Only used if `log_completions` is " "set to `True`." }, ) num_completions_to_print: int | None = field( default=None, metadata={"help": "Number of completions to print with `rich`. If `None`, all completions are logged."}, ) wandb_entity: str | None = field( default=None, metadata={"help": ("The entity to store runs under.")}, ) wandb_project: str | None = field( default=None, metadata={"help": ("The project to store runs under.")}, ) wandb_run_group: str | None = field( default=None, metadata={"help": ("The group to store runs under.")}, ) wandb_log_unique_prompts: bool = field( default=True, metadata={ "help": ("Whether to log the unique prompts to wandb. This will create a new run for each unique prompt.") }, ) callbacks: list[str] = field( default_factory=lambda: [], metadata={"help": "The callbacks to run during training."}, ) hub_model_revision: str | None = field( default="main", metadata={"help": "The Hub model branch to push the model to."} ) overwrite_hub_revision: bool = field(default=False, metadata={"help": "Whether to overwrite the Hub revision."}) push_to_hub_revision: bool = field(default=False, metadata={"help": "Whether to push to a Hub revision/branch."}) def __post_init__(self): super().__post_init__() # check lmbda and beta are in the range [0, 1] if self.lmbda < 0.0 or self.lmbda > 1.0: raise ValueError("lmbda must be in the range [0.0, 1.0].") if self.beta < 0.0 or self.beta > 1.0: raise ValueError("beta must be in the range [0.0, 1.0].") # Validate that max_length is sufficient for max_completion_length if self.max_length is not None and self.max_completion_length >= self.max_length: raise ValueError( f"max_completion_length ({self.max_completion_length}) must be smaller than max_length ({self.max_length}) " f"to leave room for the prompt. Consider increasing max_length or reducing max_completion_length." ) if self.num_generations < 1: raise ValueError(f"num_generations must be at least 1, got {self.num_generations}.") local_sequence_batch_size = self.per_device_train_batch_size * self.gradient_accumulation_steps if self.generation_batch_size is None: self.generation_batch_size = local_sequence_batch_size // self.num_generations if self.generation_batch_size < 1: raise ValueError( f"generation_batch_size must be at least 1. Got generation_batch_size={self.generation_batch_size}." ) if self.generation_batch_size * self.num_generations != local_sequence_batch_size: raise ValueError( "generation_batch_size and num_generations must exactly partition the local optimizer-step batch. " "Expected generation_batch_size * num_generations == per_device_train_batch_size * " f"gradient_accumulation_steps, got {self.generation_batch_size} * {self.num_generations} != " f"{self.per_device_train_batch_size} * {self.gradient_accumulation_steps}." ) if self.num_generations > 1 and self.lmbda < 1.0: warnings.warn( f"num_generations={self.num_generations} with lmbda={self.lmbda} means off-policy batches include " f"{self.num_generations} copies of each sample; consider lmbda=1.0 when num_generations > 1.", UserWarning, stacklevel=2, ) # Validate ULD parameters if self.use_uld_loss: if self.uld_crossentropy_weight < 0.0: raise ValueError("uld_crossentropy_weight must be non-negative.") if self.uld_distillation_weight < 0.0: raise ValueError("uld_distillation_weight must be non-negative.") if self.uld_student_temperature <= 0.0: raise ValueError("uld_student_temperature must be positive.") if self.uld_teacher_temperature <= 0.0: raise ValueError("uld_teacher_temperature must be positive.") # Validate hybrid loss weights - both must be None or both must be set if self.uld_use_hybrid_loss: if (self.uld_hybrid_matched_weight is None) != (self.uld_hybrid_unmatched_weight is None): raise ValueError( "uld_hybrid_matched_weight and uld_hybrid_unmatched_weight must both be None (for adaptive " "weighting) or both be set to numeric values. Got uld_hybrid_matched_weight=" f"{self.uld_hybrid_matched_weight} and uld_hybrid_unmatched_weight=" f"{self.uld_hybrid_unmatched_weight}." ) if self.uld_hybrid_matched_weight is not None: if self.uld_hybrid_matched_weight < 0.0: raise ValueError("uld_hybrid_matched_weight must be non-negative.") if self.uld_hybrid_unmatched_weight < 0.0: raise ValueError("uld_hybrid_unmatched_weight must be non-negative.") ================================================ FILE: trl/experimental/gold/gold_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import random import textwrap import warnings from collections import defaultdict, deque from collections.abc import Callable from contextlib import nullcontext from functools import partial from typing import Any, Optional import torch import torch.distributed as dist import torch.nn as nn import torch.nn.functional as F from accelerate import PartialState from accelerate.utils import DistributedType, broadcast_object_list, gather_object, is_peft_model from datasets import Dataset, IterableDataset from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.utils.data import DataLoader from transformers import AutoTokenizer, TrainerCallback, TrainerControl, TrainerState, is_bitsandbytes_available from transformers.data.data_collator import DataCollator from transformers.feature_extraction_utils import FeatureExtractionMixin from transformers.generation.configuration_utils import GenerationConfig from transformers.image_processing_utils import BaseImageProcessor from transformers.integrations.integration_utils import is_wandb_available from transformers.modeling_utils import PreTrainedModel from transformers.processing_utils import ProcessorMixin from transformers.tokenization_utils_base import PreTrainedTokenizerBase from transformers.trainer_utils import EvalPrediction, seed_worker from transformers.utils import ( is_datasets_available, is_flash_attn_2_available, is_liger_kernel_available, is_peft_available, is_rich_available, ) from ...data_utils import is_conversational, maybe_convert_to_chatml, pack_dataset, truncate_dataset from ...extras.profiling import profiling_decorator from ...generation.vllm_client import VLLMClient from ...import_utils import is_vllm_available from ...models import prepare_deepspeed from ...models.utils import unwrap_model_for_generation from ...trainer.sft_trainer import SFTTrainer from ...trainer.utils import ( RepeatSampler, create_model_from_path, disable_dropout_in_model, ensure_master_addr_port, pad, split_tensor_dict, ) from ..utils import DataCollatorForChatML, empty_cache from .gold_config import GOLDConfig if is_peft_available(): from peft import PeftConfig if is_wandb_available(): import wandb if is_vllm_available(): from vllm import LLM, SamplingParams from vllm.sampling_params import StructuredOutputsParams if is_liger_kernel_available(): from liger_kernel.chunked_loss import LigerFusedLinearJSDLoss if is_rich_available(): from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.text import Text if is_bitsandbytes_available(): import bitsandbytes as bnb def print_prompt_completions_sample_uld( prompts: list[str], completions: list[str], step: int, num_samples: int = None, ) -> None: """ Print out a sample of model completions to the console with multiple reward metrics. This function creates a nicely formatted table showing prompt-completion pairs, useful for monitoring model outputs during training. It requires the `rich` library to be installed. Args: prompts (`list[str]`): List of prompts. completions (`list[str]`): List of completions corresponding to the prompts. rewards (`dict[str, list[float]]`): Dictionary where keys are reward names and values are lists of rewards. advantages (`list[float]`): List of advantages corresponding to the prompts and completions. step (`int`): Current training step number, used in the output title. num_samples (`int` or `None`, *optional*, defaults to `None`): Number of random samples to display. If `None` (default), all items will be displayed. Example: ```python >>> from trl.trainer.utils import print_prompt_completions_sample >>> prompts = ["The sky is", "The sun is"] >>> completions = [" blue.", " in the sky."] >>> rewards = {"Correctness": [0.123, 0.456], "Format": [0.789, 0.101]} >>> advantages = [0.987, 0.654] >>> print_prompt_completions_sample(prompts, completions, rewards, advantages, 42) ╭──────────────────────────── Step 42 ─────────────────────────────╮ │ ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Correctness ┃ Format ┃ Advantage ┃ │ │ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ │ │ │ The sky is │ blue. │ 0.12 │ 0.79 │ 0.99 │ │ │ ├────────────┼──────────────┼─────────────┼────────┼───────────┤ │ │ │ The sun is │ in the sky. │ 0.46 │ 0.10 │ 0.65 │ │ │ └────────────┴──────────────┴─────────────┴────────┴───────────┘ │ ╰──────────────────────────────────────────────────────────────────╯ ``` """ if not is_rich_available(): raise ImportError( "The function `print_prompt_completions_sample` requires the `rich` library. Please install it with " "`pip install rich`." ) console = Console() table = Table(show_header=True, header_style="bold white", expand=True) # Add columns table.add_column("Prompt", style="bright_yellow") table.add_column("Completion", style="bright_green") # Some basic input validation if num_samples is not None: if num_samples >= len(prompts): num_samples = None elif num_samples <= 0: return # Subsample data if num_samples is specified if num_samples is not None: indices = random.sample(range(len(prompts)), num_samples) prompts = [prompts[i] for i in indices] completions = [completions[i] for i in indices] for i in range(len(prompts)): table.add_row(Text(prompts[i]), Text(completions[i])) table.add_section() # Adds a separator between rows panel = Panel(table, expand=False, title=f"Step {step}", border_style="bold white") console.print(panel) def build_teacher_inputs_from_texts( tokenizer: PreTrainedTokenizerBase, prompt_texts: list[str], completion_texts: list[str], ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]: """Tokenize teacher prompts/completions and produce tensors ready for GOLD loss.""" pad_token_id = tokenizer.pad_token_id eos_token_id = tokenizer.eos_token_id prompt_token_ids = tokenizer(prompt_texts, add_special_tokens=True)["input_ids"] completion_token_ids = tokenizer(completion_texts, add_special_tokens=False)["input_ids"] sequences: list[torch.Tensor] = [] attention_masks: list[torch.Tensor] = [] labels_list: list[torch.Tensor] = [] prompt_lengths: list[int] = [] for prompt_ids, completion_ids in zip(prompt_token_ids, completion_token_ids, strict=True): # Remove trailing EOS from prompt so completions can extend cleanly if eos_token_id is not None and prompt_ids and prompt_ids[-1] == eos_token_id: prompt_ids = prompt_ids[:-1] prompt_lengths.append(len(prompt_ids)) sequence = list(prompt_ids) sequence.extend(completion_ids) if eos_token_id is not None: sequence.append(eos_token_id) seq_tensor = torch.tensor(sequence, dtype=torch.long) sequences.append(seq_tensor) attention_masks.append(torch.ones_like(seq_tensor)) labels = seq_tensor.clone() labels[: len(prompt_ids)] = -100 if pad_token_id is not None: labels[labels == pad_token_id] = -100 labels_list.append(labels) teacher_input_ids = pad( sequences, padding_side="right", padding_value=pad_token_id if pad_token_id is not None else 0, ) teacher_attention_mask = pad(attention_masks, padding_side="right", padding_value=0).bool() teacher_labels = pad(labels_list, padding_side="right", padding_value=-100) if eos_token_id is not None: for row in range(teacher_attention_mask.size(0)): valid = ( teacher_input_ids[row] != pad_token_id if pad_token_id is not None else teacher_attention_mask[row].bool() ) if valid.any(): last_idx = valid.nonzero(as_tuple=True)[0][-1] teacher_attention_mask[row, last_idx + 1 :] = False teacher_prompt_length = max(prompt_lengths) if prompt_lengths else 0 return teacher_input_ids, teacher_labels, teacher_attention_mask, teacher_prompt_length class ULDLoss(nn.Module): """ Universal Logit Distillation Loss. """ def __init__(self, config: GOLDConfig, student_tokenizer=None, teacher_tokenizer=None, device=None): super().__init__() self.device = device self.crossentropy_weight = config.uld_crossentropy_weight self.distillation_weight = config.uld_distillation_weight self.student_temperature = config.uld_student_temperature self.teacher_temperature = config.uld_teacher_temperature self.skip_student_eos = config.uld_skip_student_eos self.skip_teacher_eos = config.uld_skip_teacher_eos self.use_extended_uld = config.use_extended_uld self.ignore_index = -100 # Add tokenizers for enhanced alignment self.student_tokenizer = student_tokenizer self.teacher_tokenizer = teacher_tokenizer # Hybrid ULD configuration self.use_hybrid_loss = getattr(config, "uld_use_hybrid_loss", False) self.hybrid_matched_weight = getattr(config, "uld_hybrid_matched_weight", None) self.hybrid_unmatched_weight = getattr(config, "uld_hybrid_unmatched_weight", None) self.beta = getattr(config, "beta", 1.0) # For JSD loss in hybrid matched tokens # Initialize vocabulary mapping for hybrid loss self._vocab_mapping = None self._teacher_matched_ids = None self._student_matched_ids = None if self.use_hybrid_loss and student_tokenizer is not None and teacher_tokenizer is not None: self._initialize_vocabulary_mapping() def __call__( self, student_logits, teacher_logits, student_labels, teacher_labels, student_input_ids, teacher_input_ids ): """ Compute ULD loss with GKD trainer interface. Args: student_logits: Student model logits [batch_size, seq_len, vocab_size] teacher_logits: Teacher model logits [batch_size, seq_len, vocab_size] student_labels: Student target labels [batch_size, seq_len] teacher_labels: Teacher target labels [batch_size, seq_len] student_input_ids: Student input token IDs [batch_size, seq_len] teacher_input_ids: Teacher input token IDs [batch_size, seq_len] Returns: Total loss (cross-entropy + distillation) """ # Compute cross-entropy loss for student if self.crossentropy_weight > 0: shift_logits = student_logits[..., :-1, :].contiguous() shift_labels = student_labels[..., 1:].contiguous() loss_fct = nn.CrossEntropyLoss(ignore_index=self.ignore_index) crossentropy_loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) crossentropy_loss = self.crossentropy_weight * crossentropy_loss else: crossentropy_loss = 0.0 # Compute distillation loss using ULD approximation distillation_loss = self._compute_distillation_loss( student_logits, teacher_logits, student_labels, teacher_labels, student_input_ids, teacher_input_ids ) return crossentropy_loss + distillation_loss def _initialize_vocabulary_mapping(self): """Initialize vocabulary mapping for hybrid ULD loss.""" # Computing vocabulary mapping for hybrid ULD student_vocab = self.student_tokenizer.get_vocab() teacher_vocab = self.teacher_tokenizer.get_vocab() # Create reverse mapping for student student_token_to_id = dict(student_vocab.items()) vocab_mapping = {} teacher_matched_ids = set() student_matched_ids = set() for token_str, teacher_id in teacher_vocab.items(): if token_str in student_token_to_id: student_id = student_token_to_id[token_str] vocab_mapping[teacher_id] = student_id teacher_matched_ids.add(teacher_id) student_matched_ids.add(student_id) self._vocab_mapping = vocab_mapping self._teacher_matched_ids = teacher_matched_ids self._student_matched_ids = student_matched_ids max_matched_teacher_id = max(self._vocab_mapping.keys()) self.mapping_tensor = torch.full((max_matched_teacher_id + 1,), -1, dtype=torch.long) # -1 for unmapped ids for k, v in self._vocab_mapping.items(): self.mapping_tensor[k] = v if self.device is not None: self.mapping_tensor = self.mapping_tensor.to(self.device) def _compute_distillation_loss( self, student_logits, teacher_logits, student_labels, teacher_labels, student_input_ids, teacher_input_ids ): """ Compute the Universal Logit Distillation loss with token mapping. This version uses actual input_ids for accurate token mapping and multiplies probabilities for split tokens. Both student_input_ids and teacher_input_ids are required for optimal alignment. """ # Get answer regions (same as original) student_answer_index, student_answer_size = self._get_start_and_size_answers(student_labels) teacher_answer_index, teacher_answer_size = self._get_start_and_size_answers(teacher_labels) if self.skip_student_eos: student_answer_size = [size - 1 for size in student_answer_size] if self.skip_teacher_eos: teacher_answer_size = [size - 1 for size in teacher_answer_size] # Handle edge case where all answer sizes are 0 if ( not student_answer_size or not teacher_answer_size or max(max(student_answer_size), max(teacher_answer_size)) <= 0 ): return torch.zeros(1, device=student_logits.device, requires_grad=True) * student_logits.sum() * 1e-8 batch_size = student_logits.size(0) distillation_losses = [] for i in range(batch_size): # Get answer regions for this batch item student_start = student_answer_index[i] student_size = student_answer_size[i] teacher_start = teacher_answer_index[i] teacher_size = teacher_answer_size[i] if student_size <= 0 or teacher_size <= 0: loss_i = student_logits[i].sum() * 0.0 distillation_losses.append(loss_i) continue # Extract answer logits student_answer_logits = student_logits[i, student_start : student_start + student_size] teacher_answer_logits = teacher_logits[i, teacher_start : teacher_start + teacher_size] # Convert to probabilities student_probs = F.softmax(student_answer_logits / self.student_temperature, dim=-1) teacher_probs = F.softmax(teacher_answer_logits / self.teacher_temperature, dim=-1) # Get token IDs for mapping (always use actual input_ids) student_token_ids = student_input_ids[i, student_start : student_start + student_size].tolist() teacher_token_ids = teacher_input_ids[i, teacher_start : teacher_start + teacher_size].tolist() if self.use_extended_uld: # Build alignment groups directly from token ids using greedy text matching student_alignment_groups, teacher_alignment_groups = self._build_alignment_groups_from_ids( student_token_ids, teacher_token_ids ) # Merge student probabilities using student alignment groups # Pass student_token_ids to enable corrected conditional probability merging student_aligned = self._merge_probabilities_with_alignment_groups( student_probs, student_alignment_groups, student_token_ids ) # Merge teacher probabilities using teacher alignment groups # Pass teacher_token_ids to enable corrected conditional probability merging teacher_aligned = self._merge_probabilities_with_alignment_groups( teacher_probs, teacher_alignment_groups, teacher_token_ids ) else: min_length = min(len(student_token_ids), len(teacher_token_ids)) student_aligned = student_probs[:min_length, :] teacher_aligned = teacher_probs[:min_length, :] # Apply ULD loss computation if self.use_hybrid_loss and self._vocab_mapping is not None: # Use hybrid approach: direct comparison for matched tokens, sorting for unmatched aligned_loss = self._compute_hybrid_uld_loss(student_aligned, teacher_aligned) else: # Original approach: sort all probabilities student_sorted = student_aligned.sort(dim=-1, descending=True).values teacher_sorted = teacher_aligned.sort(dim=-1, descending=True).values # Pad vocabularies to same size student_vocab_size = student_sorted.size(-1) teacher_vocab_size = teacher_sorted.size(-1) max_vocab_size = max(student_vocab_size, teacher_vocab_size) if student_vocab_size < max_vocab_size: student_sorted = F.pad(student_sorted, (0, max_vocab_size - student_vocab_size)) if teacher_vocab_size < max_vocab_size: teacher_sorted = F.pad(teacher_sorted, (0, max_vocab_size - teacher_vocab_size)) # Compute L1 distance (ULD approach) aligned_loss = F.l1_loss(student_sorted, teacher_sorted, reduction="sum") aligned_loss /= student_aligned.size(0) # Normalize by sequence length distillation_losses.append(aligned_loss) distillation_loss = torch.stack(distillation_losses).mean() return self.distillation_weight * distillation_loss def _build_alignment_groups_from_ids(self, student_token_ids, teacher_token_ids): """ Build alignment groups using a greedy substring-equality algorithm on decoded token pieces. Args: student_token_ids: List[int] teacher_token_ids: List[int] Returns: Tuple[List[List[int]], List[List[int]]]: student and teacher alignment groups """ def to_canonical_pieces(tok, ids): pieces = [] prev = "" for k in range(len(ids)): # IMPORTANT: Do NOT skip special tokens - we need to align them too cur = tok.decode(ids[: k + 1], skip_special_tokens=False, clean_up_tokenization_spaces=False) # Extract the incremental addition (may include spaces/ZWJ/etc.) pieces.append(cur[len(prev) :]) prev = cur return pieces s_pieces = to_canonical_pieces(self.student_tokenizer, student_token_ids) t_pieces = to_canonical_pieces(self.teacher_tokenizer, teacher_token_ids) i = j = 0 s_buf = t_buf = "" s_group = [] t_group = [] s_groups = [] t_groups = [] def flush(): if s_group and t_group: s_groups.append(s_group.copy()) t_groups.append(t_group.copy()) # Greedily accumulate pieces until substrings match, then flush while i < len(s_pieces) or j < len(t_pieces): if s_buf == t_buf and s_buf != "": flush() s_buf = t_buf = "" s_group = [] t_group = [] continue if s_buf == "" and i < len(s_pieces): s_buf += s_pieces[i] s_group.append(i) i += 1 continue if t_buf == "" and j < len(t_pieces): t_buf += t_pieces[j] t_group.append(j) j += 1 continue if len(s_buf) <= len(t_buf): if i < len(s_pieces): s_buf += s_pieces[i] s_group.append(i) i += 1 elif j < len(t_pieces): t_buf += t_pieces[j] t_group.append(j) j += 1 else: if j < len(t_pieces): t_buf += t_pieces[j] t_group.append(j) j += 1 elif i < len(s_pieces): s_buf += s_pieces[i] s_group.append(i) i += 1 # Flush any remainder if both sides accumulated something if s_buf == t_buf and s_group and t_group: flush() elif s_group or t_group: # Handle remaining unmatched tokens by forcing a flush # This ensures both sides have the same number of alignment groups if s_group or t_group: # Ensure both groups have content (even if empty list) if not s_group: s_group = [] if not t_group: t_group = [] # Force flush even if buffers don't match if s_group or t_group: s_groups.append(s_group.copy() if s_group else []) t_groups.append(t_group.copy() if t_group else []) return s_groups, t_groups def _merge_probabilities_with_alignment_groups(self, probs, alignment_groups, token_ids=None): """ Merge probabilities based on alignment groups with corrected conditional probability handling. For a group merging tokens at positions [i, i+1, ..., i+k], we compute: P_merged(y | x) = P(y | x) × P(token_{i+1} | token_i, x) × ... × P(token_{i+k} | ..., x) Where: - P(y | x) is the marginal probability distribution over all vocabulary tokens at position i - token_{i+1}, ..., token_{i+k} are the ACTUAL tokens that were generated - The conditional probabilities P(token_j | ..., x) are extracted as SCALARS - y ranges over all vocabulary tokens at position i This ensures the probability of the actual generated sequence is correct (by the chain rule), while introducing a known bias for counterfactual tokens (since we don't have P(token_{i+k} | y, x) for y != token_i). The merged distribution is unnormalized but preserves correct relative probabilities. Args: probs: Probability tensor [seq_len, vocab_size] alignment_groups: List of alignment groups (each group is a list of positions to merge) token_ids: Actual token IDs that were generated [seq_len]. REQUIRED when any group has len(group) > 1. If None when multi-token groups exist, raises ValueError. Returns: Merged probability tensor [num_groups, vocab_size] Raises: ValueError: If token_ids is None when merging multi-token groups """ if not alignment_groups: return probs # Create aligned tensor vocab_size = probs.size(-1) target_len = len(alignment_groups) aligned_probs = torch.zeros(target_len, vocab_size, device=probs.device, dtype=probs.dtype) eps = 1e-8 # Process each alignment group for group_idx, group in enumerate(alignment_groups): # Handle probability merging if len(group) > 1: # Multiple tokens map to this group - merge using corrected conditional probability approach if token_ids is None: raise ValueError( "token_ids must be provided when merging multi-token groups. " "This is required for mathematically correct probability merging." ) # Start with the marginal distribution at the first position first_pos = group[0] marginal_probs = probs[first_pos] # P(y | x₀) for all y # For each subsequent token in the group, extract the SCALAR conditional probability # of the actual token that was generated, and multiply conditional_prob_product = 1.0 for idx in group[1:]: # Get the actual token ID that was generated at this position actual_token_id = token_ids[idx] # Extract its probability (scalar) token_prob = probs[idx, actual_token_id].clamp_min(eps) conditional_prob_product *= token_prob # Merge: multiply the scalar conditional prob product with the entire marginal distribution # This gives: P(y | x_0) × P(token_1 | token_0, x) × ... × P(token_k | ..., x) # Note: This is unnormalized, but preserves the correct joint probability for the actual sequence merged_probs = marginal_probs * conditional_prob_product aligned_probs[group_idx] = merged_probs elif len(group) == 1: aligned_probs[group_idx] = probs[group[0]] else: # No tokens map to this group aligned_probs[group_idx] = torch.zeros_like(probs[0]) return aligned_probs def _compute_hybrid_uld_loss(self, student_aligned, teacher_aligned): """ Compute hybrid ULD loss on aligned probability distributions. This method: 1. Directly compares probabilities for tokens with matching vocabulary entries 2. Uses sorting approach only for tokens with different vocabulary entries Args: student_aligned: Aligned student probabilities [seq_len, student_vocab_size] teacher_aligned: Aligned teacher probabilities [seq_len, teacher_vocab_size] Returns: Combined hybrid loss """ device = student_aligned.device # seq_len = student_aligned.size(0) # Unused variable student_vocab_size = student_aligned.size(-1) teacher_vocab_size = teacher_aligned.size(-1) # Convert sets to sorted tensors for indexing if self._teacher_matched_ids: teacher_matched_indices = torch.tensor(sorted(self._teacher_matched_ids), dtype=torch.long, device=device) student_matched_indices = self.mapping_tensor[teacher_matched_indices] else: teacher_matched_indices = torch.tensor([], dtype=torch.long, device=device) student_matched_indices = torch.tensor([], dtype=torch.long, device=device) # Create masks for unmatched tokens teacher_matched_mask = torch.zeros(teacher_vocab_size, dtype=torch.bool, device=device) student_matched_mask = torch.zeros(student_vocab_size, dtype=torch.bool, device=device) if len(teacher_matched_indices) > 0: teacher_matched_mask[teacher_matched_indices] = True student_matched_mask[student_matched_indices] = True # 1. JSD loss for matched vocabulary tokens (direct semantic correspondence) matched_loss = torch.tensor(0.0, device=device) matched_token_count = 0 if len(teacher_matched_indices) > 0: # Extract probabilities for matched tokens teacher_matched_probs = teacher_aligned[:, teacher_matched_indices] # [seq_len, num_matched] student_matched_probs = student_aligned[:, student_matched_indices] # [seq_len, num_matched] matched_token_count = teacher_matched_probs.size(-1) # Use JSD loss for semantically aligned tokens # Convert probabilities back to logits for JSD computation # Apply generalized JSD loss to matched tokens matched_loss = self._compute_jsd_loss_for_matched_tokens(student_matched_probs, teacher_matched_probs) # 2. Sorted comparison loss for unmatched vocabulary tokens teacher_unmatched_mask = ~teacher_matched_mask student_unmatched_mask = ~student_matched_mask teacher_unmatched_probs = teacher_aligned[:, teacher_unmatched_mask] # [seq_len, num_teacher_unmatched] student_unmatched_probs = student_aligned[:, student_unmatched_mask] # [seq_len, num_student_unmatched] unmatched_loss = torch.tensor(0.0, device=device) if teacher_unmatched_probs.size(-1) > 0 and student_unmatched_probs.size(-1) > 0: # Sort unmatched probabilities teacher_unmatched_sorted = teacher_unmatched_probs.sort(dim=-1, descending=True).values student_unmatched_sorted = student_unmatched_probs.sort(dim=-1, descending=True).values # Pad to same size if needed teacher_unmatched_size = teacher_unmatched_sorted.size(-1) student_unmatched_size = student_unmatched_sorted.size(-1) max_unmatched_size = max(teacher_unmatched_size, student_unmatched_size) if teacher_unmatched_size < max_unmatched_size: teacher_unmatched_sorted = F.pad( teacher_unmatched_sorted, (0, max_unmatched_size - teacher_unmatched_size) ) if student_unmatched_size < max_unmatched_size: student_unmatched_sorted = F.pad( student_unmatched_sorted, (0, max_unmatched_size - student_unmatched_size) ) # L1 loss on sorted unmatched tokens unmatched_loss = F.l1_loss(student_unmatched_sorted, teacher_unmatched_sorted, reduction="sum") unmatched_loss /= student_aligned.size(0) # Normalize by sequence length # 3. Combine losses with weights if self.hybrid_matched_weight is None: # Use adaptive weighting based on vocabulary overlap hybrid_matched_weight = matched_token_count / max(1, teacher_vocab_size) hybrid_unmatched_weight = 1.0 - hybrid_matched_weight else: # Use fixed weights provided in config hybrid_matched_weight = self.hybrid_matched_weight hybrid_unmatched_weight = self.hybrid_unmatched_weight total_loss = hybrid_matched_weight * matched_loss + hybrid_unmatched_weight * unmatched_loss # Store matched/unmatched components for logging self.last_matched_loss = matched_loss self.last_unmatched_loss = unmatched_loss return total_loss def _compute_jsd_loss_for_matched_tokens(self, student_logits, teacher_logits): """ Compute JSD loss for matched vocabulary tokens. Args: student_logits: Student logits for matched tokens [seq_len, num_matched] teacher_logits: Teacher logits for matched tokens [seq_len, num_matched] Returns: JSD loss for matched tokens """ # Reshape to [batch_size * seq_len, vocab_size] format expected by generalized_jsd_loss batch_seq_len, num_matched = student_logits.shape student_logits_reshaped = student_logits.view(-1, num_matched) teacher_logits_reshaped = teacher_logits.view(-1, num_matched) # Use the GOLD generalized JSD loss implementation that accepts probability inputs jsd_loss = GOLDTrainer.generalized_jsd_loss( student_logits_reshaped, teacher_logits_reshaped, labels=None, # No masking needed for matched tokens beta=self.beta, # Standard JSD beta temperature=1.0, # Already applied in main computation reduction="batchmean", logits_are_probs=True, ) return jsd_loss def _get_start_and_size_answers(self, answer_tensors): answers_index = [] answers_size = [] for answer in answer_tensors: answer_mask = answer.ne(self.ignore_index) if not answer_mask.any(): answers_index.append(0) answers_size.append(0) continue valid_indices = answer_mask.nonzero(as_tuple=True)[0] answers_index.append(int(valid_indices[0].item())) answers_size.append(int(answer_mask.sum().item())) return answers_index, answers_size class GOLDVLLMSyncCallback(TrainerCallback): """Sync the model weights to vLLM after training steps when it's safe to do so.""" def __init__(self, trainer): self.trainer = trainer def on_step_end(self, args, state: TrainerState, control: TrainerControl, **kwargs): """Sync weights after training step when DeepSpeed is stable.""" if ( self.trainer.use_vllm and state.global_step != self.trainer._last_vllm_sync_step and state.global_step % self.trainer.vllm_sync_frequency == 0 ): # Check if this is a step where gradients are synchronized # This happens at the end of gradient accumulation cycles if hasattr(self.trainer.accelerator, "sync_gradients") and self.trainer.accelerator.sync_gradients: self.trainer._move_model_to_vllm() self.trainer._last_vllm_sync_step = state.global_step class GOLDTrainer(SFTTrainer): _tag_names = ["trl", "gold"] _name = "GOLD" _paper = { "title": "Unlocking On-Policy Distillation for Any Model Family", # docstyle-ignore "citation": textwrap.dedent("""\ @misc{patino2025unlocking, title = {{Unlocking On-Policy Distillation for Any Model Family}}, author = {Carlos Miguel Patiño and Kashif Rasul and Quentin Gallouédec and Ben Burtenshaw and Sergio Paniego and Vaibhav Srivastav and Thibaud Frere and Ed Beeching and Lewis Tunstall and Leandro von Werra and Thomas Wolf}, year = 2025, url = {https://huggingface.co/spaces/HuggingFaceH4/general-on-policy-logit-distillation}, }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str | None = None, teacher_model: PreTrainedModel | nn.Module | str = None, args: GOLDConfig | None = None, data_collator: DataCollator | None = None, # type: ignore train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: Optional["PeftConfig"] = None, ): self.model_name_or_path = model if isinstance(model, str) else model.config._name_or_path self.model_revision = (args.model_init_kwargs or {}).get("revision") # Respect a user-provided data_collator; otherwise, provide a ChatML collator that if data_collator is None: data_collator = DataCollatorForChatML(tokenizer=processing_class, max_length=args.max_length) # Liger fused GKD loss (JSD) self.use_liger_gkd_loss = False if args.use_liger_kernel: self.liger_jsd_loss = LigerFusedLinearJSDLoss( beta=args.beta, ignore_index=-100, temperature=args.temperature, compiled=False, weight_hard_loss=0.0, weight_soft_loss=1.0, ) self.use_liger_gkd_loss = True if args.teacher_model_init_kwargs is None: teacher_model_init_kwargs = {} elif not isinstance(teacher_model, str): raise ValueError( "You passed teacher_model_init_kwargs to the GOLDConfig, but your teacher_model is already instantiated." ) else: teacher_model_init_kwargs = args.teacher_model_init_kwargs teacher_model_init_kwargs["torch_dtype"] = ( teacher_model_init_kwargs["torch_dtype"] if teacher_model_init_kwargs["torch_dtype"] in ["auto", None] else getattr(torch, teacher_model_init_kwargs["torch_dtype"]) ) if args.use_uld_loss and args.teacher_tokenizer_name_or_path is None: if isinstance(teacher_model, str): args.teacher_tokenizer_name_or_path = teacher_model else: raise ValueError( "`teacher_tokenizer_name_or_path` must be set when using ULD loss with a pre-instantiated teacher model." ) if isinstance(teacher_model, str): init_kwargs = dict(teacher_model_init_kwargs) if args.teacher_model_revision is not None: init_kwargs.setdefault("revision", args.teacher_model_revision) if "torch_dtype" in init_kwargs and "dtype" not in init_kwargs: init_kwargs["dtype"] = init_kwargs.pop("torch_dtype") teacher_model = create_model_from_path(teacher_model, **init_kwargs) self.use_uld_loss = args.use_uld_loss self.teacher_tokenizer = None if args.use_uld_loss and args.teacher_tokenizer_name_or_path is not None: self.teacher_tokenizer = AutoTokenizer.from_pretrained(args.teacher_tokenizer_name_or_path) if not hasattr(self.teacher_tokenizer, "pad_token") or self.teacher_tokenizer.pad_token is None: self.teacher_tokenizer.pad_token = self.teacher_tokenizer.eos_token # Hybrid ULD loss configuration is handled in ULDLoss class super().__init__( model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, peft_config=peft_config, ) if args.disable_dropout: disable_dropout_in_model(self.model) if not args.use_uld_loss: teacher_model.resize_token_embeddings(self.model.config.vocab_size) if self.is_deepspeed_enabled: self.teacher_model = prepare_deepspeed(teacher_model, self.accelerator) else: self.teacher_model = self.accelerator.prepare_model(teacher_model, evaluation_mode=True) self.lmbda = args.lmbda self.beta = args.beta self.temperature = args.temperature self.top_p = args.top_p self.seq_kd = args.seq_kd self.num_generations = args.num_generations # Track per-step loss statistics for on/off-policy batches (used in logging) self._on_policy_loss_total = 0.0 self._off_policy_loss_total = 0.0 self._on_policy_step_equiv = 0.0 self._off_policy_step_equiv = 0.0 # Buffering for rollouts across gradient accumulation steps self._buffered_inputs = None self._buffered_on_policy = None self._buffered_text_logs = None self._step = 0 # Hybrid ULD matched/unmatched accumulators (logged every step when ULD hybrid is used) self._matched_sum = 0.0 self._unmatched_sum = 0.0 self._matched_step_eq = 0.0 self._unmatched_step_eq = 0.0 self.use_transformers_paged = args.use_transformers_paged or False self.uld_loss_fn = None if self.use_uld_loss: self.uld_loss_fn = ULDLoss( config=args, student_tokenizer=processing_class, teacher_tokenizer=self.teacher_tokenizer, device=self.accelerator.device, ) generation_kwargs = { "max_new_tokens": args.max_completion_length, "temperature": args.temperature, "top_p": args.top_p, "do_sample": True, "top_k": args.top_k, "pad_token_id": self.processing_class.pad_token_id, } self.generation_config = GenerationConfig(**generation_kwargs) # Keep training-specific generation kwargs to overwrite model's original generation config self.generation_kwargs = generation_kwargs if ( hasattr(self.model.generation_config, "eos_token_id") and self.model.generation_config.eos_token_id is not None ): self.generation_config.eos_token_id = self.model.generation_config.eos_token_id # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 self.log_completions = args.log_completions self.log_completion_steps = args.log_completions_steps self.wandb_log_unique_prompts = args.wandb_log_unique_prompts self.num_completions_to_print = args.num_completions_to_print # maxlen is set to the total number of forward passes per step. This value of `maxlen` ensures we log only the # final optimization step. maxlen = self.accelerator.num_processes * args.per_device_train_batch_size * args.gradient_accumulation_steps self._textual_logs = { "prompt": deque(maxlen=maxlen), "completion": deque(maxlen=maxlen), "rewards": defaultdict(lambda: deque(maxlen=maxlen)), "advantages": deque(maxlen=maxlen), } self.use_vllm = args.use_vllm if self.use_vllm: if not is_vllm_available(): raise ImportError( "vLLM is not available and use_vllm is set to True. Please install vLLM with " "`pip install vllm` to use it." ) self.vllm_mode = args.vllm_mode self.vllm_tensor_parallel_size = args.vllm_tensor_parallel_size self.vllm_gpu_memory_utilization = args.vllm_gpu_memory_utilization self.vllm_enable_sleep_mode = args.vllm_enable_sleep_mode if self.vllm_mode == "server": if self.accelerator.is_main_process: self.vllm_client = VLLMClient( host=args.vllm_server_host, server_port=args.vllm_server_port, connection_timeout=args.vllm_server_timeout, ) self.vllm_client.init_communicator() elif self.vllm_mode == "colocate": student_model_name_or_path = self.model_name_or_path # Make sure tensor_parallel_size divides world size evenly if not self.accelerator.num_processes % self.vllm_tensor_parallel_size == 0: raise ValueError( f"vllm_tensor_parallel_size ({self.vllm_tensor_parallel_size}) must divide world size " f"({self.accelerator.num_processes}) evenly." ) if self.vllm_tensor_parallel_size > 1: # Create subgroups of ranks for TP self.vllm_tp_group, _ = torch.distributed.new_subgroups_by_enumeration( [ list( range( i * self.vllm_tensor_parallel_size, (i + 1) * self.vllm_tensor_parallel_size, ) ) for i in range(self.accelerator.num_processes // self.vllm_tensor_parallel_size) ] ) # vLLM requires the environment variables to be set for distributed training. os.environ["RANK"] = str(self.accelerator.process_index) os.environ["LOCAL_RANK"] = str(self.accelerator.local_process_index) os.environ["WORLD_SIZE"] = str(self.accelerator.num_processes) ensure_master_addr_port() vllm_quantization = None if is_bitsandbytes_available(): for _, module in model.named_modules(): if isinstance(module, bnb.nn.Linear4bit): vllm_quantization = "bitsandbytes" break elif isinstance(module, bnb.nn.Linear8bitLt): raise ValueError("vLLM does not support in-flight 8-bit quantization.") self.vllm_engine = LLM( model=student_model_name_or_path, revision=self.model_revision, tensor_parallel_size=self.vllm_tensor_parallel_size, gpu_memory_utilization=self.vllm_gpu_memory_utilization, max_num_seqs=self.args.per_device_train_batch_size * self.args.gradient_accumulation_steps, max_model_len=args.max_length, distributed_executor_backend="external_launcher", # Feed identical seed for tp groups to ensure sampling results are the same across workers seed=self.accelerator.process_index // self.vllm_tensor_parallel_size, enable_sleep_mode=self.vllm_enable_sleep_mode, quantization=vllm_quantization, ) if self.vllm_enable_sleep_mode: self.vllm_engine.sleep(level=2) # When using vLLM, the main process is responsible for loading the model weights. This can cause process # desynchronization and seems to lead to DeepSpeed hanging during initialization. To prevent this, we # synchronize all processes after vLLM has been fully initialized. self.accelerator.wait_for_everyone() else: raise ValueError(f"Unknown vllm_mode: {self.vllm_mode}") self.vllm_structured_outputs_regex = args.vllm_structured_outputs_regex self.vllm_sync_frequency = args.vllm_sync_frequency self._last_vllm_sync_step = -1 self.add_callback(GOLDVLLMSyncCallback(self)) def _set_signature_columns_if_needed(self): super()._set_signature_columns_if_needed() required_columns = [ "prompts", "prompt_attention_mask", "messages", "chat_template_kwargs", "tools", "original_prompt_text", "original_completion_text", ] if self._signature_columns is None: self._signature_columns = required_columns else: for column in required_columns: if column not in self._signature_columns: self._signature_columns.append(column) def _get_train_sampler(self, dataset=None): if dataset is None: dataset = self.train_dataset return RepeatSampler( data_source=dataset, mini_repeat_count=self.num_generations, batch_size=self.args.generation_batch_size * self.accelerator.num_processes, repeat_count=self.args.gradient_accumulation_steps, shuffle=True, seed=self.args.seed, ) def get_train_dataloader(self): """ Override Trainer.get_train_dataloader to load one generation batch per optimizer window. The dataloader yields local batches of size `per_device_train_batch_size * gradient_accumulation_steps`. The `RepeatSampler` (with `repeat_count=gradient_accumulation_steps`) ensures each generation batch is sampled `gradient_accumulation_steps` times so Trainer's loop iterates the correct number of times. Only the first batch in each window triggers `_fill_buffer`; the rest are ignored by `_prepare_inputs`. """ if self.train_dataset is None: raise ValueError("Trainer: training requires a train_dataset.") train_dataset = self.train_dataset data_collator = self.data_collator if is_datasets_available() and isinstance(train_dataset, Dataset): train_dataset = self._remove_unused_columns(train_dataset, description="training") else: data_collator = self._get_collator_with_removed_columns(data_collator, description="training") dataloader_params = { "batch_size": self._train_batch_size * self.args.gradient_accumulation_steps, "collate_fn": data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "persistent_workers": self.args.dataloader_persistent_workers, } if not isinstance(train_dataset, torch.utils.data.IterableDataset): dataloader_params["sampler"] = self._get_train_sampler() dataloader_params["drop_last"] = self.args.dataloader_drop_last dataloader_params["worker_init_fn"] = partial( seed_worker, num_workers=self.args.dataloader_num_workers, rank=self.args.process_index, ) if self.args.dataloader_num_workers > 0: dataloader_params["prefetch_factor"] = self.args.dataloader_prefetch_factor return self.accelerator.prepare(DataLoader(train_dataset, **dataloader_params)) @profiling_decorator def _prepare_inputs(self, generation_batch: dict[str, torch.Tensor | Any]) -> dict[str, torch.Tensor | Any]: if not self.model.training: return generation_batch buffer_steps = self.args.gradient_accumulation_steps if self._step % buffer_steps == 0 or self._buffered_inputs is None: self._fill_buffer(generation_batch, buffer_steps) slice_idx = self._step % buffer_steps inputs = self._buffered_inputs[slice_idx] self._step += 1 return inputs def _decode_completion_texts_from_labels(self, slice_inputs: dict[str, torch.Tensor | Any]) -> list[str] | None: """Decode completion text from labels when raw text is absent.""" labels = slice_inputs.get("labels") if labels is None or not isinstance(labels, torch.Tensor): return None labels_cpu = labels.detach().cpu() decoded_completion_tokens: list[list[int]] = [] for row in labels_cpu: token_ids = row[row != -100].tolist() if self.processing_class.pad_token_id is not None: token_ids = [tok for tok in token_ids if tok != self.processing_class.pad_token_id] decoded_completion_tokens.append(token_ids) return self.processing_class.batch_decode( decoded_completion_tokens, skip_special_tokens=False, clean_up_tokenization_spaces=False, ) def _ensure_original_text_fields( self, slice_inputs: dict[str, torch.Tensor | Any] ) -> dict[str, torch.Tensor | Any]: """Populate original prompt/completion text fields when missing.""" if "original_prompt_text" in slice_inputs and "original_completion_text" in slice_inputs: return slice_inputs prompts = slice_inputs.get("prompts") if prompts is None or not isinstance(prompts, torch.Tensor): return slice_inputs prompt_texts = self.processing_class.batch_decode( prompts, skip_special_tokens=False, clean_up_tokenization_spaces=False, ) completion_texts = self._decode_completion_texts_from_labels(slice_inputs) if completion_texts is None: return slice_inputs updated_slice = dict(slice_inputs) updated_slice["original_prompt_text"] = prompt_texts updated_slice["original_completion_text"] = completion_texts return updated_slice @staticmethod def _build_sequence_batch( new_input_ids: torch.Tensor, prompt_lengths: torch.Tensor, pad_token_id: int | None ) -> tuple[torch.Tensor, torch.Tensor]: """Build attention mask and labels from full sequences and prompt lengths.""" prompt_lengths = prompt_lengths.to(device=new_input_ids.device, dtype=torch.long) positions = torch.arange(new_input_ids.shape[1], device=new_input_ids.device).unsqueeze(0) completion_mask = positions >= prompt_lengths.unsqueeze(1) new_attention_mask = torch.ones_like(new_input_ids) if pad_token_id is not None: new_attention_mask[new_input_ids == pad_token_id] = 0 new_labels = torch.full_like(new_input_ids, -100) new_labels[completion_mask] = new_input_ids[completion_mask] if pad_token_id is not None: new_labels[new_input_ids == pad_token_id] = -100 return new_attention_mask, new_labels @profiling_decorator def _fill_buffer(self, generation_batch: dict[str, torch.Tensor | Any], buffer_steps: int): slices = split_tensor_dict(generation_batch, buffer_steps) if self.accelerator.is_main_process: on_policy_flags = [random.random() <= self.lmbda for _ in range(buffer_steps)] else: on_policy_flags = [False] * buffer_steps on_policy_flags = broadcast_object_list(on_policy_flags, from_process=0) on_policy_indices = [i for i, flag in enumerate(on_policy_flags) if flag] self._buffered_inputs = [None] * buffer_steps self._buffered_on_policy = on_policy_flags self._buffered_text_logs = [None] * buffer_steps for i, flag in enumerate(on_policy_flags): if not flag: slice_inputs = slices[i] if self.use_uld_loss and self.teacher_tokenizer is not None: slice_inputs = self._ensure_original_text_fields(slice_inputs) if "original_prompt_text" not in slice_inputs or "original_completion_text" not in slice_inputs: raise ValueError( "Off-policy batch missing 'original_prompt_text' or 'original_completion_text' fields. " "When using ULD loss with cross-tokenizer alignment, datasets must be prepared with " "_prepare_dataset_with_original_text(). Ensure your dataset includes these fields." ) self._buffered_inputs[i] = slice_inputs if on_policy_indices: self._generate_on_policy_for_slices(slices, on_policy_indices) @profiling_decorator def _generate_on_policy_for_slices( self, slices: list[dict[str, torch.Tensor | Any]], on_policy_indices: list[int] ): local_prompts = [] local_slice_indices = [] for slice_idx in on_policy_indices: slice_inputs = slices[slice_idx] for prompt in slice_inputs["prompts"]: local_prompts.append(prompt) local_slice_indices.append(slice_idx) prompts_text_for_vllm = self.processing_class.batch_decode( torch.stack(local_prompts) if local_prompts else torch.empty(0, dtype=torch.long), skip_special_tokens=True, ) if self.processing_class.pad_token: prompts_text_for_vllm = [p.replace(self.processing_class.pad_token, "") for p in prompts_text_for_vllm] prompts_text_with_special = self.processing_class.batch_decode( torch.stack(local_prompts) if local_prompts else torch.empty(0, dtype=torch.long), skip_special_tokens=False, ) if self.use_vllm: self._wake_vllm_if_needed() max_completion_length = self.generation_config.max_new_tokens temperature = self.generation_config.temperature top_k = ( self.generation_config.top_k if self.generation_config.top_k and self.generation_config.top_k > 0 else -1 ) top_p = self.args.top_p if hasattr(self.args, "top_p") else 1.0 repetition_penalty = self.args.repetition_penalty if hasattr(self.args, "repetition_penalty") else 1.0 min_p = self.args.min_p if hasattr(self.args, "min_p") else 0.0 if self.use_vllm and self.vllm_mode == "server": completion_ids = self._generate_vllm_server_global( prompts_text_for_vllm, max_completion_length, temperature, top_k, top_p, repetition_penalty, min_p, n=self.num_generations, ) elif self.use_vllm and self.vllm_mode == "colocate": completion_ids = self._generate_vllm_colocate( prompts_text_for_vllm, max_completion_length, temperature, top_k, top_p, repetition_penalty, min_p, n=self.num_generations, ) else: self._generate_non_vllm_for_slices(slices, on_policy_indices) return self._process_completions_to_buffer( slices, on_policy_indices, local_slice_indices, completion_ids, prompts_text_for_vllm, prompts_text_with_special, max_completion_length, ) @staticmethod def _deduplicate_prompts( prompts: list[str], num_generations: int ) -> tuple[list[str], list[tuple[int, int]]] | None: """Deduplicate prompts and build a completion remapping.""" seen: dict[str, list[int]] = {} unique_prompts: list[str] = [] dedup_mapping: list[tuple[int, int]] = [] for prompt in prompts: if prompt not in seen: seen[prompt] = [len(unique_prompts), 0] unique_prompts.append(prompt) entry = seen[prompt] if entry[1] >= num_generations: return None dedup_mapping.append((entry[0], entry[1])) entry[1] += 1 return unique_prompts, dedup_mapping def _generate_vllm_server_global( self, prompts_text: list[str], max_tokens: int, temperature: float, top_k: int, top_p: float, repetition_penalty: float, min_p: float, n: int = 1, ) -> list: all_prompts_text = gather_object(prompts_text) local_count = len(prompts_text) if self.accelerator.is_main_process: if all_prompts_text: dedup_mapping = None if n > 1: dedup_result = self._deduplicate_prompts(all_prompts_text, n) if dedup_result is not None: gen_prompts, dedup_mapping = dedup_result gen_n = n else: gen_prompts = all_prompts_text gen_n = 1 else: gen_prompts = all_prompts_text gen_n = 1 completion_ids = self.vllm_client.generate( prompts=gen_prompts, n=gen_n, repetition_penalty=repetition_penalty, temperature=temperature, top_p=top_p, top_k=top_k, min_p=min_p, max_tokens=max_tokens, structured_outputs_regex=self.vllm_structured_outputs_regex, )["completion_ids"] if dedup_mapping is not None: completion_ids = [completion_ids[uid * gen_n + gid] for uid, gid in dedup_mapping] else: completion_ids = [] else: completion_ids = [None] * len(all_prompts_text) if all_prompts_text else [] completion_ids = broadcast_object_list(completion_ids, from_process=0) process_slice = slice( self.accelerator.process_index * local_count, (self.accelerator.process_index + 1) * local_count, ) return completion_ids[process_slice] def _generate_vllm_colocate( self, prompts_text: list[str], max_tokens: int, temperature: float, top_k: int, top_p: float, repetition_penalty: float, min_p: float, n: int = 1, ) -> list: if self.vllm_structured_outputs_regex: structured_outputs = StructuredOutputsParams(backend="outlines", regex=self.vllm_structured_outputs_regex) else: structured_outputs = None if hasattr(self, "vllm_tp_group") and self.vllm_tensor_parallel_size > 1: orig_size = len(prompts_text) gathered_prompts = [None for _ in range(self.vllm_tensor_parallel_size)] torch.distributed.all_gather_object(gathered_prompts, prompts_text, group=self.vllm_tp_group) all_prompts_text = [p for sublist in gathered_prompts for p in sublist] else: all_prompts_text = prompts_text dedup_mapping = None if n > 1 and all_prompts_text: dedup_result = self._deduplicate_prompts(all_prompts_text, n) if dedup_result is not None: gen_prompts, dedup_mapping = dedup_result gen_n = n else: gen_prompts = all_prompts_text gen_n = 1 else: gen_prompts = all_prompts_text gen_n = 1 sampling_params = SamplingParams( n=gen_n, repetition_penalty=repetition_penalty, temperature=temperature, top_p=top_p, top_k=top_k, min_p=min_p, max_tokens=max_tokens, structured_outputs=structured_outputs, ) if gen_prompts: all_outputs = self.vllm_engine.generate(gen_prompts, sampling_params=sampling_params, use_tqdm=False) completion_ids = [output.token_ids for outputs in all_outputs for output in outputs.outputs] else: completion_ids = [] if dedup_mapping is not None: completion_ids = [completion_ids[uid * gen_n + gid] for uid, gid in dedup_mapping] if hasattr(self, "vllm_tp_group") and self.vllm_tensor_parallel_size > 1: local_rank_in_group = torch.distributed.get_rank(group=self.vllm_tp_group) tp_slice = slice(local_rank_in_group * orig_size, (local_rank_in_group + 1) * orig_size) completion_ids = completion_ids[tp_slice] if self.vllm_enable_sleep_mode: self.vllm_engine.sleep(level=2) return completion_ids def _generate_non_vllm_for_slices(self, slices: list[dict[str, torch.Tensor | Any]], on_policy_indices: list[int]): """Fallback generation without vLLM (uses model.generate per slice).""" with unwrap_model_for_generation( self.model, self.accelerator, generation_kwargs=self.generation_kwargs, ) as unwrapped_model: for slice_idx in on_policy_indices: slice_inputs = slices[slice_idx] result = self.generate_on_policy_outputs( unwrapped_model, slice_inputs, self.generation_config, self.processing_class.pad_token_id, ) new_input_ids, new_attention_mask, new_labels, prompt_texts, completion_texts = result updated_slice = dict(slice_inputs) updated_slice["input_ids"] = new_input_ids updated_slice["attention_mask"] = new_attention_mask updated_slice["labels"] = new_labels updated_slice["original_prompt_text"] = prompt_texts updated_slice["original_completion_text"] = completion_texts self._buffered_inputs[slice_idx] = updated_slice self._buffered_text_logs[slice_idx] = (prompt_texts, completion_texts) def _process_completions_to_buffer( self, slices: list[dict[str, torch.Tensor | Any]], on_policy_indices: list[int], local_slice_indices: list[int], completion_ids: list, prompts_text: list[str], prompts_text_with_special: list[str], max_completion_length: int, ): """ Process vLLM completions and update buffered inputs for on-policy slices. """ device = self.accelerator.device pad_token_id = self.processing_class.pad_token_id if self.processing_class.pad_token_id is not None else 0 slice_completions = {idx: [] for idx in on_policy_indices} slice_prompts = {idx: [] for idx in on_policy_indices} slice_prompts_special = {idx: [] for idx in on_policy_indices} for i, slice_idx in enumerate(local_slice_indices): slice_completions[slice_idx].append(completion_ids[i]) slice_prompts[slice_idx].append(prompts_text[i]) slice_prompts_special[slice_idx].append(prompts_text_with_special[i]) for slice_idx in on_policy_indices: slice_inputs = slices[slice_idx] completion_ids_for_slice = slice_completions[slice_idx] prompt_txts = slice_prompts[slice_idx] prompt_txts_with_special = slice_prompts_special[slice_idx] prompt_max_length = max(1, self.args.max_length - max_completion_length) if self.args.max_length else None prompt_tokenized = self.processing_class( prompt_txts, return_tensors="pt", padding="longest", padding_side="left", truncation=True if prompt_max_length else False, max_length=prompt_max_length, add_special_tokens=False, ).to(device) prompt_ids = prompt_tokenized.input_ids completion_ids_tensors = [torch.tensor(ids, device=device) for ids in completion_ids_for_slice] completion_ids_for_text: list[list[int]] = [] padded_completion_ids_list = [] for completion_tensor in completion_ids_tensors: if len(completion_tensor) > max_completion_length: truncated_completion_tensor = completion_tensor[:max_completion_length] padded_completion_ids_list.append(truncated_completion_tensor) completion_ids_for_text.append(truncated_completion_tensor.tolist()) elif len(completion_tensor) < max_completion_length: padding_needed = max_completion_length - len(completion_tensor) padded_tensor = torch.cat( [ completion_tensor, torch.full( (padding_needed,), pad_token_id, device=device, dtype=completion_tensor.dtype, ), ] ) padded_completion_ids_list.append(padded_tensor) completion_ids_for_text.append(completion_tensor.tolist()) else: padded_completion_ids_list.append(completion_tensor) completion_ids_for_text.append(completion_tensor.tolist()) completion_ids_padded = torch.stack(padded_completion_ids_list) new_input_ids = torch.cat([prompt_ids, completion_ids_padded], dim=1) prompt_lengths = torch.full((prompt_ids.shape[0],), prompt_ids.shape[1], device=device) new_attention_mask, new_labels = self._build_sequence_batch(new_input_ids, prompt_lengths, pad_token_id) completion_texts = self.processing_class.batch_decode( completion_ids_for_text, skip_special_tokens=False, clean_up_tokenization_spaces=False, ) updated_slice = dict(slice_inputs) updated_slice["input_ids"] = new_input_ids updated_slice["attention_mask"] = new_attention_mask updated_slice["labels"] = new_labels updated_slice["original_prompt_text"] = prompt_txts_with_special updated_slice["original_completion_text"] = completion_texts self._buffered_inputs[slice_idx] = updated_slice self._buffered_text_logs[slice_idx] = (prompt_txts, completion_texts) def _prepare_dataset( self, dataset: Dataset | IterableDataset, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin, args, packing: bool, formatting_func: Callable[[dict], str] | None, dataset_name: str, ) -> Dataset | IterableDataset: """Preserve original text fields for ULD when needed.""" column_names = list(next(iter(dataset)).keys()) is_processed = "input_ids" in column_names if not is_processed or (self.use_uld_loss and self.teacher_tokenizer is not None): return self._prepare_dataset_with_original_text( dataset, processing_class, args, packing, formatting_func, dataset_name ) return super()._prepare_dataset(dataset, processing_class, args, packing, formatting_func, dataset_name) def _prepare_dataset_with_original_text( self, dataset: Dataset | IterableDataset, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin, args, packing: bool, formatting_func: Callable[[dict], str] | None, dataset_name: str, ) -> Dataset | IterableDataset: """ Prepare dataset while preserving original text for cross-tokenizer distillation. """ # Build the kwargs for the `map` function map_kwargs = {} if isinstance(dataset, Dataset): # IterableDataset does not support num_proc map_kwargs["num_proc"] = args.dataset_num_proc with PartialState().main_process_first(): # Apply the formatting function if any if formatting_func is not None: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Applying formatting function to {dataset_name} dataset" def _func(example): return {"text": formatting_func(example)} dataset = dataset.map(_func, batched=False, **map_kwargs) # Convert the dataset to ChatML if needed if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Converting {dataset_name} dataset to ChatML" column_names = next(iter(dataset)).keys() dataset = dataset.map( maybe_convert_to_chatml, remove_columns="conversations" if "conversations" in column_names else None, **map_kwargs, ) # Apply the chat template if needed and preserve original text first_example = next(iter(dataset)) if not is_conversational(first_example): if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Adding EOS to {dataset_name} dataset" def add_eos(example, eos_token): if "text" in example and not example["text"].endswith(eos_token): # language modeling case example["text"] = example["text"] + eos_token elif "completion" in example and not example["completion"].endswith(eos_token): example["completion"] = example["completion"] + eos_token return example dataset = dataset.map( add_eos, fn_kwargs={"eos_token": processing_class.eos_token}, remove_columns="messages" if "messages" in column_names else None, # renamed to "text" **map_kwargs, ) # Tokenize the dataset while preserving original text if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset (preserving original text)" def tokenize_with_original_text(example, processing_class, dataset_text_field, assistant_only_loss): """Modified tokenization function that preserves original text.""" result = {} if "prompt" in example: # prompt-completion case # Store original text result["original_prompt_text"] = example["prompt"] result["original_completion_text"] = example["completion"] if is_conversational(example): prompt_ids = processing_class.apply_chat_template( example["prompt"], return_dict=False, **example.get("chat_template_kwargs", {}) ) prompt_completion_ids = processing_class.apply_chat_template( example["prompt"] + example["completion"], return_dict=False, **example.get("chat_template_kwargs", {}), ) else: prompt_ids = processing_class(text=example["prompt"]).input_ids prompt_completion_ids = processing_class( text=example["prompt"] + example["completion"] ).input_ids # Check if the tokenized prompt starts with the tokenized prompt+completion if not prompt_completion_ids[: len(prompt_ids)] == prompt_ids: warnings.warn( "Mismatch between tokenized prompt and the start of tokenized prompt+completion. " "This may be due to unexpected tokenizer behavior, whitespace issues, or special " "token handling. Verify that the tokenizer is processing text consistently.", stacklevel=2, ) # Create a completion mask completion_mask = [0] * len(prompt_ids) + [1] * (len(prompt_completion_ids) - len(prompt_ids)) result.update( { "input_ids": prompt_completion_ids, "completion_mask": completion_mask, "attention_mask": [1] * len(prompt_completion_ids), # Add attention mask } ) else: # language modeling or conversational case if is_conversational(example): # For conversational data (ChatML), extract prompt and completion properly messages = example["messages"] # Extract user and assistant messages separately user_messages = [msg for msg in messages if msg["role"] != "assistant"] assistant_messages = [msg for msg in messages if msg["role"] == "assistant"] if user_messages and assistant_messages: # Apply chat template to get the prompt (everything up to assistant) prompt_text = processing_class.apply_chat_template( user_messages, add_generation_prompt=True, # add assistant prompt tokenize=False, **example.get("chat_template_kwargs", {}), ) # Get the full conversation with assistant response full_text = processing_class.apply_chat_template( messages, add_generation_prompt=False, tokenize=False, **example.get("chat_template_kwargs", {}), ) # Extract completion as everything after the prompt # This ensures we capture any extra tokens (like tags) that the template adds if full_text.startswith(prompt_text): completion_text = full_text[len(prompt_text) :] else: # Fallback: use assistant content + EOS assistant_content = assistant_messages[0]["content"] completion_text = ( assistant_content + processing_class.eos_token if hasattr(processing_class, "eos_token") else assistant_content ) # Store original text for cross-tokenizer distillation result["original_prompt_text"] = prompt_text result["original_completion_text"] = completion_text else: # Fallback: use empty prompt and full text as completion full_text = processing_class.apply_chat_template( messages, tokenize=False, **example.get("chat_template_kwargs", {}) ) result["original_prompt_text"] = "" result["original_completion_text"] = full_text # Process the conversation normally processed = processing_class.apply_chat_template( example["messages"], return_dict=True, return_assistant_tokens_mask=assistant_only_loss, **example.get("chat_template_kwargs", {}), ) if "assistant_masks" in processed and 1 not in processed["assistant_masks"]: raise RuntimeError( "You're using `assistant_only_loss=True`, but at least one example has no " "assistant tokens. This usually means the tokenizer's chat template doesn't " "generate assistant masks — it may be missing the `{% generation %}` tag. Please " "check the template and ensure it's correctly configured to support assistant " "masking." ) result.update({k: processed[k] for k in ("input_ids", "assistant_masks") if k in processed}) # Add attention_mask if not already present if "attention_mask" not in result: result["attention_mask"] = [1] * len(result["input_ids"]) else: # For regular language modeling, store the full text as completion and empty prompt result["original_prompt_text"] = "" result["original_completion_text"] = example.get(dataset_text_field, example.get("text", "")) tokenized = processing_class(text=example[dataset_text_field]) result.update( { "input_ids": tokenized.input_ids, "attention_mask": getattr(tokenized, "attention_mask", [1] * len(tokenized.input_ids)), } ) return result dataset = dataset.map( tokenize_with_original_text, fn_kwargs={ "processing_class": processing_class, "dataset_text_field": args.dataset_text_field, "assistant_only_loss": args.assistant_only_loss, }, **map_kwargs, ) # Pack or truncate if packing: if args.max_length is None: raise ValueError("When packing is enabled, `max_length` can't be `None`.") if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Packing {dataset_name} dataset" columns_to_keep = ["input_ids", "original_prompt_text", "original_completion_text"] existing_columns = set(dataset.column_names) columns_to_select = [col for col in columns_to_keep if col in existing_columns] dataset = dataset.select_columns(columns_to_select) dataset = pack_dataset(dataset, args.max_length, args.packing_strategy, map_kwargs) elif args.max_length is not None: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Truncating {dataset_name} dataset" dataset = truncate_dataset(dataset, args.max_length, map_kwargs=map_kwargs) if args.use_liger_kernel: required_columns = { "input_ids", "attention_mask", "position_ids", "completion_mask", "messages", "assistant_masks", "original_prompt_text", "original_completion_text", } dataset = dataset.select_columns(required_columns.intersection(dataset.column_names)) return dataset @staticmethod def generalized_jsd_loss( student_logits, teacher_logits, labels=None, beta=0.5, temperature=1.0, reduction="batchmean", logits_are_probs=False, ): """ Compute the generalized Jensen-Shannon Divergence loss for knowledge distillation using F.kl_div. See Eq. (1) of https://huggingface.co/papers/2306.13649 for the definition. Args: student_logits: Tensor of shape (batch_size, sequence_length, vocab_size) teacher_logits: Tensor of shape (batch_size, sequence_length, vocab_size) labels: Tensor of shape (batch_size, sequence_length) with -100 for padding tokens to ignore when computing loss beta: Interpolation coefficient between 0 and 1 (default: 0.5) temperature: Softmax temperature (default: 1.0) reduction: Specifies the reduction to apply to the output (default: 'batchmean') Returns: loss: Scalar tensor with the generalized JSD loss """ if logits_are_probs: student_log_probs = torch.log(student_logits.clamp_min(1e-8)) teacher_log_probs = torch.log(teacher_logits.clamp_min(1e-8)) else: # Apply temperature scaling to logits before computing probabilities student_logits = student_logits / temperature teacher_logits = teacher_logits / temperature # Compute log probabilities for student and probabilities for teacher student_log_probs = F.log_softmax(student_logits, dim=-1) teacher_log_probs = F.log_softmax(teacher_logits, dim=-1) if beta == 0: jsd = F.kl_div(student_log_probs, teacher_log_probs, reduction="none", log_target=True) elif beta == 1: jsd = F.kl_div(teacher_log_probs, student_log_probs, reduction="none", log_target=True) else: # Compute the log of the mixture distribution # log(a + b) = log(exp(log(a)) + exp(log(b))) -> for mixture beta = torch.tensor(beta, dtype=student_log_probs.dtype, device=student_log_probs.device) mixture_log_probs = torch.logsumexp( torch.stack([student_log_probs + torch.log1p(-beta), teacher_log_probs + torch.log(beta)]), dim=0, ) # Compute KL divergences using F.kl_div # PyTorch differs from the standard mathematical definition, so the order of the probability distributions is swapped compared to that defined in the paper. kl_teacher = F.kl_div(mixture_log_probs, teacher_log_probs, reduction="none", log_target=True) kl_student = F.kl_div(mixture_log_probs, student_log_probs, reduction="none", log_target=True) # Compute the Generalized Jensen-Shannon Divergence jsd = beta * kl_teacher + (1 - beta) * kl_student # Masking if labels is not None: mask = labels != -100 jsd = jsd[mask] # Apply reduction if reduction == "batchmean": return jsd.sum() / mask.sum() if labels is not None else jsd.sum() / jsd.size(0) elif reduction == "sum": return jsd.sum() elif reduction == "mean": return jsd.mean() else: return jsd def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): if self.use_uld_loss and self.teacher_tokenizer is not None: if "original_prompt_text" in inputs and "original_completion_text" in inputs: prompt_texts = inputs["original_prompt_text"] completion_texts = inputs["original_completion_text"] full_texts = [p + c for p, c in zip(prompt_texts, completion_texts, strict=True)] else: # Fallback: decode student input_ids (current approach) # WARNING: This may not work perfectly for cross-tokenizer distillation full_sequences = inputs["input_ids"] full_texts = self.processing_class.batch_decode(full_sequences, skip_special_tokens=False) # Try to split prompt/completion using original prompt length prompt_lengths = inputs["prompts"].shape[1] prompt_texts = self.processing_class.batch_decode(inputs["prompts"], skip_special_tokens=False) completion_texts = [ full.replace(prompt, "", 1) for full, prompt in zip(full_texts, prompt_texts, strict=True) ] ( teacher_input_ids, teacher_labels, teacher_attention_mask, teacher_prompt_length, ) = build_teacher_inputs_from_texts( self.teacher_tokenizer, prompt_texts, completion_texts, ) teacher_input_ids = teacher_input_ids.to(self.accelerator.device) teacher_labels = teacher_labels.to(self.accelerator.device) teacher_attention_mask = teacher_attention_mask.to(self.accelerator.device) outputs_student = model( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], use_cache=False, ) self.teacher_model.eval() with torch.no_grad(): outputs_teacher = self.teacher_model( input_ids=teacher_input_ids, attention_mask=teacher_attention_mask, ) # These are not used for ULD loss but are needed if JSD loss were to be used in this branch student_prompt_length = inputs["prompts"].shape[1] shifted_student_logits = outputs_student.logits[:, student_prompt_length - 1 : -1, :] shifted_teacher_logits = outputs_teacher.logits[:, teacher_prompt_length - 1 : -1, :] shifted_labels = inputs["labels"][:, student_prompt_length:] else: if self.use_liger_gkd_loss: # Forward only through the base models (avoid lm_head to save memory) unwrapped_student = self.accelerator.unwrap_model(model) if hasattr(unwrapped_student, "get_decoder") and unwrapped_student.get_decoder() is not None: base_student = unwrapped_student.get_decoder() else: base_student = getattr( unwrapped_student, getattr(unwrapped_student, "base_model_prefix", "model"), unwrapped_student ) student_outputs = base_student( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], use_cache=False, ) self.teacher_model.eval() unwrapped_teacher = self.accelerator.unwrap_model(self.teacher_model) if hasattr(unwrapped_teacher, "get_decoder") and unwrapped_teacher.get_decoder() is not None: base_teacher = unwrapped_teacher.get_decoder() else: base_teacher = getattr( unwrapped_teacher, getattr(unwrapped_teacher, "base_model_prefix", "model"), unwrapped_teacher ) with torch.no_grad(): teacher_outputs = base_teacher( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], use_cache=False, ) student_hidden = student_outputs.last_hidden_state[:, :-1] teacher_hidden = teacher_outputs.last_hidden_state[:, :-1] del student_outputs, teacher_outputs student_hidden = student_hidden.reshape(-1, student_hidden.shape[-1]) teacher_hidden = teacher_hidden.reshape(-1, teacher_hidden.shape[-1]) labels_mask = inputs["labels"] != -100 masked_input_ids = torch.where( labels_mask, inputs["input_ids"], torch.full_like(inputs["input_ids"], -100) ) true_labels = masked_input_ids[:, 1:].contiguous().reshape(-1) student_head = unwrapped_student.get_output_embeddings() teacher_head = unwrapped_teacher.get_output_embeddings() loss = self.liger_jsd_loss( student_input=student_hidden, student_weight=student_head.weight, teacher_input=teacher_hidden, teacher_weight=teacher_head.weight, true_labels=true_labels, student_bias=getattr(student_head, "bias", None), teacher_bias=getattr(teacher_head, "bias", None), ) del student_hidden, teacher_hidden, true_labels else: outputs_student = model( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], ) self.teacher_model.eval() with torch.no_grad(): outputs_teacher = self.teacher_model( input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], ) prompt_lengths = inputs["prompts"].shape[1] shifted_student_logits = outputs_student.logits[:, prompt_lengths - 1 : -1, :] shifted_teacher_logits = outputs_teacher.logits[:, prompt_lengths - 1 : -1, :] shifted_labels = inputs["labels"][:, prompt_lengths:] loss = self.generalized_jsd_loss( student_logits=shifted_student_logits, teacher_logits=shifted_teacher_logits, labels=shifted_labels, beta=self.beta, temperature=self.temperature, ) if self.use_uld_loss: student_input_ids = inputs["input_ids"] teacher_labels_for_loss = teacher_labels if "teacher_labels" in locals() else inputs["labels"] teacher_input_ids_for_loss = teacher_input_ids if "teacher_input_ids" in locals() else inputs["input_ids"] student_labels = inputs["labels"].clone() if hasattr(self.processing_class, "pad_token_id") and self.processing_class.pad_token_id is not None: student_labels[student_labels == self.processing_class.pad_token_id] = -100 if ( hasattr(self, "teacher_tokenizer") and hasattr(self.teacher_tokenizer, "pad_token_id") and self.teacher_tokenizer.pad_token_id is not None ): teacher_labels[teacher_labels == self.teacher_tokenizer.pad_token_id] = -100 loss = self.uld_loss_fn( student_logits=outputs_student.logits, teacher_logits=outputs_teacher.logits, student_labels=student_labels, teacher_labels=teacher_labels_for_loss, student_input_ids=student_input_ids, teacher_input_ids=teacher_input_ids_for_loss, ) if hasattr(self.uld_loss_fn, "last_matched_loss") and hasattr(self.uld_loss_fn, "last_unmatched_loss"): ga = max(1, int(self.args.gradient_accumulation_steps)) step_eq = 1.0 / ga matched_val = ( self.uld_loss_fn.last_matched_loss.item() if self.uld_loss_fn.last_matched_loss is not None else 0.0 ) unmatched_val = ( self.uld_loss_fn.last_unmatched_loss.item() if self.uld_loss_fn.last_unmatched_loss is not None else 0.0 ) self._matched_sum += matched_val self._unmatched_sum += unmatched_val self._matched_step_eq += step_eq self._unmatched_step_eq += step_eq empty_cache() return (loss, outputs_student) if return_outputs else loss def generate_on_policy_outputs(self, model, inputs, generation_config, pad_token_id=None): # Generate output with respect to the prompt only if self.use_transformers_paged: previous_attn = self.model.config._attn_implementation if is_flash_attn_2_available(): model.config._attn_implementation = "paged_attention" else: model.config._attn_implementation = "sdpa_paged" prompt_mask = inputs.get("prompt_attention_mask") prompts_tensor = inputs["prompts"] if prompt_mask is not None: prompt_sequences = [ row[mask.bool()].detach().cpu().tolist() for row, mask in zip(prompts_tensor, prompt_mask, strict=True) ] else: prompt_sequences = [row.detach().cpu().tolist() for row in prompts_tensor] generated_outputs = model.generate_batch(prompt_sequences, generation_config=generation_config) model.config._attn_implementation = previous_attn completion_ids = [output.generated_tokens for output in generated_outputs.values()] generated_tokens = torch.stack([torch.tensor(ids, device=model.device) for ids in completion_ids]) else: generated_outputs = model.generate( input_ids=inputs["prompts"], attention_mask=inputs.get("prompt_attention_mask", None), generation_config=generation_config, return_dict_in_generate=True, ) # Get the generated token IDs generated_tokens = generated_outputs.sequences batch_size = generated_tokens.size(0) device = generated_tokens.device prompt_mask = inputs.get("prompt_attention_mask") pad_token_id = pad_token_id if pad_token_id is not None else self.processing_class.pad_token_id if self.use_transformers_paged: # generate_batch() returns completion-only tokens, so the entire tensor is completion. prompt_lengths = torch.zeros(batch_size, dtype=torch.long, device=device) else: # model.generate() returns full sequences (prompt + completion), so completions start # after the full padded prompt width. prompt_lengths = torch.full( (batch_size,), inputs["prompts"].shape[1], dtype=torch.long, device=device, ) new_input_ids = generated_tokens new_attention_mask, new_labels = self._build_sequence_batch(new_input_ids, prompt_lengths, pad_token_id) prompt_texts = [] completion_texts = [] for idx in range(batch_size): length = int(prompt_lengths[idx].item()) prompt_tokens = inputs["prompts"][idx] if prompt_mask is not None: prompt_tokens = prompt_tokens[prompt_mask[idx].bool()] elif pad_token_id is not None: prompt_tokens = prompt_tokens[prompt_tokens != pad_token_id] prompt_texts.append( self.processing_class.decode( prompt_tokens.tolist(), skip_special_tokens=False, clean_up_tokenization_spaces=False, ) ) completion_tokens = new_input_ids[idx, length:] completion_texts.append( self.processing_class.decode( completion_tokens.tolist(), skip_special_tokens=False, clean_up_tokenization_spaces=False, ) ) return new_input_ids, new_attention_mask, new_labels, prompt_texts, completion_texts def _sync_fsdp_params_to_vllm(self, module: nn.Module, prefix: str = "", visited=None): """Memory-efficient post-order traversal of FSDP modules to extract full parameters and sync with student vLLM.""" if visited is None: visited = set() for child_name, child_module in module.named_children(): child_prefix = f"{prefix}.{child_name}" if prefix else child_name # recurse into the child self._sync_fsdp_params_to_vllm(child_module, prefix=child_prefix, visited=visited) if isinstance(module, FSDP): with FSDP.summon_full_params(module, recurse=False, writeback=False): for param_name, param in module.named_parameters(): full_name = f"{prefix}.{param_name}" if prefix else param_name for extra in ("_fsdp_wrapped_module.", "_checkpoint_wrapped_module."): full_name = full_name.replace(extra, "") if full_name in visited: continue # skip FSDP subtrees already traversed visited.add(full_name) if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(full_name, param.data) elif self.vllm_mode == "colocate": llm_model = self.vllm_engine.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(full_name, param.data)]) def _move_model_to_vllm(self): """Synchronize student model weights to vLLM engine.""" # For DeepSpeed ZeRO-3 and FSDP, we need to gather all parameters before operations deepspeed_plugin = self.accelerator.state.deepspeed_plugin zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 if zero_stage_3: import deepspeed gather_if_zero3 = deepspeed.zero.GatheredParameters else: gather_if_zero3 = nullcontext if self.vllm_mode == "colocate" and self.vllm_enable_sleep_mode: empty_cache() self.vllm_engine.wake_up(tags=["weights"]) # Work around for https://github.com/vllm-project/vllm/issues/29341 self.vllm_engine.collective_rpc("reload_weights") if is_peft_model(self.model): # With PEFT and FSDP/DeepSpeed ZeRO Stage 3, we must gather the full model at once before merging, as # merging adapters in a sharded manner is not supported. with gather_if_zero3(list(self.model.parameters())): self.model.merge_adapter() # Update vLLM weights while parameters are gathered if self.is_fsdp_enabled: # note if using FSDP, gather_if_zero3 is nullcontext # Update vLLM weights while parameters are gathered # For PEFT with FSDP we need to use the memory efficient post-order traversal self._sync_fsdp_params_to_vllm(self.model) else: # DeepSpeed ZeRO-3 with PEFT for name, param in self.model.named_parameters(): # When using PEFT, we need to recover the original parameter name and discard some parameters name = name.removeprefix("base_model.model.").replace(".base_layer", "") if self.model.prefix in name: continue # When module to save, remove its prefix and discard the original module if "original_module" in name: continue name = name.replace("modules_to_save.default.", "") if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.vllm_mode == "colocate": llm_model = self.vllm_engine.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Unmerge adapters while parameters are still gathered self.model.unmerge_adapter() # Parameters will automatically be repartitioned when exiting the context else: # For non-PEFT models, simply gather (if needed) and update each parameter individually. if self.is_fsdp_enabled: # use memory-efficient post-order traversal for FSDP self._sync_fsdp_params_to_vllm(self.model) else: # For DeepSpeed ZeRO-3, gather each parameter individually like GRPO trainer for name, param in self.model.named_parameters(): with gather_if_zero3([param]): if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.vllm_mode == "colocate": llm_model = self.vllm_engine.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Reset cache on vLLM if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.reset_prefix_cache() elif self.vllm_mode == "colocate": self.vllm_engine.reset_prefix_cache() def _wake_vllm_if_needed(self): if self.vllm_mode == "colocate" and self.vllm_enable_sleep_mode: empty_cache() self.vllm_engine.wake_up(tags=["kv_cache"]) def _get_liger_zero3_lm_head_gather_ctx(self, model: nn.Module): if not self.use_liger_gkd_loss: return nullcontext() deepspeed_plugin = self.accelerator.state.deepspeed_plugin if deepspeed_plugin is None or deepspeed_plugin.zero_stage != 3: return nullcontext() import deepspeed unwrapped_student = self.accelerator.unwrap_model(model) unwrapped_teacher = self.accelerator.unwrap_model(self.teacher_model) student_head = unwrapped_student.get_output_embeddings() teacher_head = unwrapped_teacher.get_output_embeddings() params = [student_head.weight, teacher_head.weight] if student_head.bias is not None: params.append(student_head.bias) if teacher_head.bias is not None: params.append(teacher_head.bias) return deepspeed.zero.GatheredParameters(params, modifier_rank=None) @profiling_decorator def training_step( self, model: nn.Module, inputs: dict[str, torch.Tensor | Any], num_items_in_batch: int | None = None ) -> torch.Tensor: """ Perform a training step for the General Online Logit Distillation (GOLD) model. This method implements the on-policy learning approach described in the GOLD blog post. With probability `self.lmbda`, it generates new responses using the student model, which are then used for training instead of the offline original inputs. """ buffer_steps = self.args.gradient_accumulation_steps # Keep lm_head gathered across forward+backward for Liger + ZeRO-3. with self._get_liger_zero3_lm_head_gather_ctx(model): loss = super().training_step(model, inputs, num_items_in_batch) slice_idx = (self._step - 1) % buffer_steps on_policy = False if self._buffered_on_policy is not None and slice_idx < len(self._buffered_on_policy): on_policy = self._buffered_on_policy[slice_idx] if on_policy and self._buffered_text_logs is not None and self._buffered_text_logs[slice_idx] is not None: prompt_texts, completion_texts = self._buffered_text_logs[slice_idx] self._textual_logs["prompt"].extend(gather_object(prompt_texts)) self._textual_logs["completion"].extend(gather_object(completion_texts)) loss_scalar = float(loss.detach()) step_equiv = 1.0 / self.args.gradient_accumulation_steps if on_policy: self._on_policy_loss_total += loss_scalar self._on_policy_step_equiv += step_equiv else: self._off_policy_loss_total += loss_scalar self._off_policy_step_equiv += step_equiv return loss def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics if mode == "train": device = self.accelerator.device if hasattr(self.accelerator, "device") else torch.device("cpu") vec = torch.tensor( [ self._on_policy_loss_total, self._off_policy_loss_total, self._on_policy_step_equiv, self._off_policy_step_equiv, self._matched_sum, self._unmatched_sum, self._matched_step_eq, self._unmatched_step_eq, ], dtype=torch.float64, device=device, ) if ( getattr(self.accelerator, "distributed_type", DistributedType.NO) != DistributedType.NO and dist.is_available() and dist.is_initialized() ): dist.all_reduce(vec, op=dist.ReduceOp.SUM) ( on_sum, off_sum, on_eq, off_eq, matched_sum, unmatched_sum, matched_eq, unmatched_eq, ) = vec.tolist() if on_eq > 0: logs["on_policy_loss"] = round(on_sum / on_eq, 4) if off_eq > 0: logs["off_policy_loss"] = round(off_sum / off_eq, 4) if matched_eq > 0: logs["matched_loss"] = round(matched_sum / matched_eq, 4) if unmatched_eq > 0: logs["unmatched_loss"] = round(unmatched_sum / unmatched_eq, 4) self._on_policy_loss_total = self._off_policy_loss_total = 0.0 self._on_policy_step_equiv = self._off_policy_step_equiv = 0.0 self._matched_sum = self._unmatched_sum = 0.0 self._matched_step_eq = self._unmatched_step_eq = 0.0 # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() if ( self.accelerator.is_main_process and self.log_completions and ((self.state.global_step % self.log_completion_steps) == 0) ): if is_rich_available(): print_prompt_completions_sample_uld( self._textual_logs["prompt"], self._textual_logs["completion"], self.state.global_step, self.num_completions_to_print, ) if self.args.report_to and "wandb" in self.args.report_to and wandb.run is not None: import pandas as pd table = { "step": [str(self.state.global_step)] * len(self._textual_logs["prompt"]), "prompt": self._textual_logs["prompt"], "completion": self._textual_logs["completion"], } df = pd.DataFrame(table) if self.wandb_log_unique_prompts: df = df.drop_duplicates(subset=["prompt"]) if self.num_completions_to_print and len(df) > 0: df = df.sample(n=self.num_completions_to_print, random_state=42) wandb.log({"completions": wandb.Table(dataframe=df)}) ================================================ FILE: trl/experimental/grpo_with_replay_buffer/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .grpo_with_replay_buffer_config import GRPOWithReplayBufferConfig from .grpo_with_replay_buffer_trainer import GRPOWithReplayBufferTrainer, ReplayBuffer ================================================ FILE: trl/experimental/grpo_with_replay_buffer/grpo_with_replay_buffer_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from ...trainer.grpo_config import GRPOConfig @dataclass class GRPOWithReplayBufferConfig(GRPOConfig): """ New Parameters: replay_buffer_size (`int`, *optional*, defaults to `0`): A cache that stores the rollouts with the highest advantage scores and variance per group. If a new group has 0 variance, it is replaced with a group sampled from the replay buffer. """ replay_buffer_size: int = field( default=64, metadata={ "help": "A cache that stores the rollouts with the highest advantage scores and variance per group. If a new group has 0 variance, it is replaced with a group sampled from the replay buffer." }, ) ================================================ FILE: trl/experimental/grpo_with_replay_buffer/grpo_with_replay_buffer_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import heapq from typing import Any import torch from accelerate.utils import gather_object from ...data_utils import apply_chat_template, is_conversational, prepare_multimodal_messages from ...models.utils import disable_gradient_checkpointing from ...trainer.grpo_trainer import GRPOTrainer from ...trainer.utils import nanmax, nanmin, nanstd, pad from .grpo_with_replay_buffer_config import GRPOWithReplayBufferConfig class ReplayBuffer: """ A simple replay buffer to store and sample previously seen rollouts. """ def __init__(self, max_size: int): self.max_size = max_size self.heap = [] # Min-heap of (score, data) tuples def add(self, scores: list[float], data: list[dict]): for score, datum in zip(scores, data, strict=True): if len(self.heap) < self.max_size: heapq.heappush(self.heap, (score, datum)) else: # Only add if score is better than worst (minimum) item if score > self.heap[0][0]: heapq.heapreplace(self.heap, (score, datum)) def sample(self, num_samples: int) -> list[dict[str, torch.Tensor]]: if not self.heap: return None # Sample by normalized scores scores = torch.tensor([item[0] for item in self.heap], dtype=torch.float32) probabilities = scores / scores.sum() replacement = False if num_samples > len(self.heap): replacement = True chosen_indices = torch.multinomial(probabilities, num_samples, replacement=replacement).tolist() return [self.heap[i][1] for i in chosen_indices] class GRPOWithReplayBufferTrainer(GRPOTrainer): def __init__(self, args: GRPOWithReplayBufferConfig | None = None, **kwargs): super().__init__(args=args, **kwargs) self.replay_buffer = ReplayBuffer(args.replay_buffer_size) if args.replay_buffer_size > 0 else None def _generate_and_score_completions( self, inputs: list[dict[str, torch.Tensor | Any]] ) -> dict[str, torch.Tensor | Any]: device = self.accelerator.device mode = "train" if self.model.training else "eval" prompts = [x["prompt"] for x in inputs] if "images" in inputs[0]: images = [example.get("images") for example in inputs] elif "image" in inputs[0]: images = [[example.get("image")] if example.get("image") is not None else None for example in inputs] else: images = None # Transformers requires at least one image in the batch, otherwise it throws an error if images is not None and all(img_list == [] for img_list in images): images = None # If the prompts are conversational and the inputs contain images, we need to convert the prompts from # [{"role": "user", "content": "What color is the sky?"}] to # [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What color is the sky?"}]}] if images is not None: if not is_conversational(inputs[0]): raise ValueError( "Multimodal training requires conversational prompts. It looks like the dataset contains " "non-conversational inputs, likely because a chat template was applied before passing the dataset " "to the trainer. Please provide the raw conversational prompts and let the trainer apply the chat " "template internally." ) prompts = [ prepare_multimodal_messages(prompt, image_list) for prompt, image_list in zip(prompts, images, strict=True) ] ( prompt_ids_list, completion_ids_list, tool_mask_list, completions, num_items_in_batch, sampling_per_token_logps_list, extra_fields, ) = self._generate(prompts) # Convert lists of token IDs to padded tensors prompt_ids = [torch.tensor(ids) for ids in prompt_ids_list] prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] prompt_ids = pad( prompt_ids, padding_value=self.pad_token_id, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) prompt_mask = pad( prompt_mask, padding_value=0, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_ids = [torch.tensor(ids) for ids in completion_ids_list] completion_mask = [torch.ones_like(ids, dtype=torch.long) for ids in completion_ids] completion_ids = pad( completion_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) if sampling_per_token_logps_list is not None: sampling_per_token_logps = [torch.tensor(logps) for logps in sampling_per_token_logps_list] sampling_per_token_logps = pad( sampling_per_token_logps, padding_value=0.0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) else: sampling_per_token_logps = None if self.tools: tool_mask = [torch.tensor(mask) for mask in tool_mask_list] tool_mask = pad( tool_mask, padding_value=1, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) # 0 for tool result tokens, 1 elsewhere # If mask_truncated_completions is enabled, zero out truncated completions in completion_mask if self.mask_truncated_completions: eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids_list], device=device) completion_mask = completion_mask * (~is_truncated).unsqueeze(1).int() # Concatenate prompt_mask with completion_mask for logit computation prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # (B, P+C) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size num_images = [len(img_list) for img_list in images] if images is not None else None # Get forward_kwargs for models with multimodal inputs if images is not None: prompts_text = [ apply_chat_template( {"prompt": prompt}, self.processing_class, tools=self.tools, **self.chat_template_kwargs )["prompt"] for prompt in prompts ] prompt_inputs = self.processing_class(images=images, text=prompts_text, padding=True, return_tensors="pt") prompt_inputs = super()._prepare_inputs(prompt_inputs) forward_kwargs = {k: v for k, v in prompt_inputs.items() if k not in ["input_ids", "attention_mask"]} else: forward_kwargs = {} # If token_type_ids are used, extend them with zeros for the completion part if "token_type_ids" in forward_kwargs: token_type_ids = forward_kwargs["token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and token_type_ids must have equal len padding_size = prompt_ids.size(1) - token_type_ids.size(1) if padding_size > 0: token_type_ids = torch.cat( [token_type_ids.new_zeros((token_type_ids.size(0), padding_size)), token_type_ids], dim=1 ) forward_kwargs["token_type_ids"] = torch.cat( [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # If mm_token_type_ids are used, extend them with zeros for the completion part if "mm_token_type_ids" in forward_kwargs: mm_token_type_ids = forward_kwargs["mm_token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and mm_token_type_ids must have equal len padding_size = prompt_ids.size(1) - mm_token_type_ids.size(1) if padding_size > 0: mm_token_type_ids = torch.cat( [mm_token_type_ids.new_zeros((mm_token_type_ids.size(0), padding_size)), mm_token_type_ids], dim=1, ) forward_kwargs["mm_token_type_ids"] = torch.cat( [mm_token_type_ids, mm_token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # When gradient checkpointing is enabled with use_reentrant=True (non default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): # If the generation and optimization steps are misaligned—i.e., if generation does not occur at the end of # a full optimizer step (when gradient_accumulation_steps is not a multiple of generate_every)—then the # samples may come from an earlier version of the model. In that case, we need to track old_per_token_logps # for importance sampling. If the steps are aligned, importance sampling isn't necessary and we set # old_per_token_logps to None. # When using vLLM, we always compute old_per_token_logps for importance sampling, it was shown that the # distribution mismatch between vLLM and the training model can be large and harm the training. generate_every = self.args.steps_per_generation * self.num_iterations # generation frequency if self.args.gradient_accumulation_steps % generate_every != 0 or ( self.use_vllm and self.vllm_importance_sampling_correction ): old_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: old_per_token_logps = None # Compute the importance sampling ratio when using vLLM, to correct for potential distribution mismatch if self.use_vllm and self.vllm_importance_sampling_correction: importance_sampling_ratio = torch.exp(old_per_token_logps - sampling_per_token_logps) importance_sampling_ratio = torch.clamp( importance_sampling_ratio, max=self.vllm_importance_sampling_cap ) # Compute the per-token log probabilities for the reference model if self.beta != 0.0: if self.ref_model is not None: ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.ref_model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: with self.accelerator.unwrap_model(self.model).disable_adapter(): ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: ref_per_token_logps = None # Decode prompts_text = self.processing_class.batch_decode(prompt_ids, skip_special_tokens=True) completions_text = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Merge extra_fields from rollout_func into inputs for reward functions if extra_fields: for i, inp in enumerate(inputs): for key, values in extra_fields.items(): if isinstance(values, list) and i < len(values): inp[key] = values[i] elif not isinstance(values, list): inp[key] = values # Calculate rewards for each reward function. rewards_per_func aggregates rewards across all processes. This is # important because rewards will be normalized per group, and completions are distributed. We will later slice # rewards_per_func to extract each process's subset. rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list) # Apply weights to each reward function's output and sum rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) # Compute grouped-wise rewards mean_grouped_rewards = rewards.view(-1, self.num_generations).mean(dim=1) # Normalize the rewards to compute the advantages mean_grouped_rewards = mean_grouped_rewards.repeat_interleave(self.num_generations, dim=0) advantages = rewards - mean_grouped_rewards grouped_std_rewards = rewards.view(-1, self.num_generations).std(dim=1) grouped_std_rewards = grouped_std_rewards.repeat_interleave(self.num_generations, dim=0) if self.scale_rewards in ["group", "none"]: # If self.scale_rewards = "none", we'll still log group level std std_rewards = grouped_std_rewards.clone() elif self.scale_rewards == "batch": # Compute global std std_rewards = rewards.std().expand_as(rewards) else: raise ValueError( f"Invalid value for scale_rewards: {self.scale_rewards}. Must be one of 'batch', 'group', or 'none'." ) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) if self.scale_rewards != "none": advantages = advantages / (std_rewards + 1e-4) # Slice to keep only the local part of the data process_slice = slice( self.accelerator.process_index * len(prompts), (self.accelerator.process_index + 1) * len(prompts), ) all_process_advantages = advantages.clone() # keep the aggregated advantages for logging advantages = advantages[process_slice] grouped_std_rewards = grouped_std_rewards[process_slice] # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) for i, reward_func_name in enumerate(self.reward_func_names): mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) std_func_rewards = nanstd(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_func_rewards) self._metrics[mode]["reward"].append(mean_grouped_rewards.mean().item()) self._metrics[mode]["reward_std"].append(std_rewards.mean().item()) self._metrics[mode]["frac_reward_zero_std"].append(is_std_zero.float().mean().item()) # Log prompt and completion texts self._logs["prompt"].extend(gather_object(prompts_text)) self._logs["completion"].extend(gather_object(completions_text)) for i, name in enumerate(self.reward_func_names): self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) self._logs["advantages"].extend(all_process_advantages.tolist()) if images is not None: self._logs["images"].extend(gather_object(images)) if self.use_vllm and self.vllm_importance_sampling_correction: delta = torch.abs(old_per_token_logps - sampling_per_token_logps) mask = completion_mask.bool() if not self.tools else (completion_mask * tool_mask).bool() delta = delta[mask] mean_delta = torch.mean(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) max_delta = torch.max(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) self._metrics[mode]["sampling/sampling_logp_difference/mean"].append( self.accelerator.gather(mean_delta).mean().item() ) self._metrics[mode]["sampling/sampling_logp_difference/max"].append( self.accelerator.gather(max_delta).max().item() ) flat_is_ratio = importance_sampling_ratio[mask] min_importance_sampling_ratio = ( torch.min(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) mean_importance_sampling_ratio = ( torch.mean(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) max_importance_sampling_ratio = ( torch.max(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( nanmin(self.accelerator.gather(min_importance_sampling_ratio)).item() ) self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item() ) self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( nanmax(self.accelerator.gather(max_importance_sampling_ratio)).item() ) outputs_after_sampling_buffer = self.update_with_replay_buffer( advantages, grouped_std_rewards, prompt_ids, prompt_mask, completion_ids, completion_mask, forward_kwargs, num_items_in_batch, old_per_token_logps, ref_per_token_logps, importance_sampling_ratio if self.use_vllm and self.vllm_importance_sampling_correction else None, ) if outputs_after_sampling_buffer is not None: return outputs_after_sampling_buffer else: output = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "advantages": advantages, "num_items_in_batch": num_items_in_batch, } if old_per_token_logps is not None: output["old_per_token_logps"] = old_per_token_logps if self.use_vllm and self.vllm_importance_sampling_correction: output["importance_sampling_ratio"] = importance_sampling_ratio if ref_per_token_logps is not None: output["ref_per_token_logps"] = ref_per_token_logps if "pixel_values" in forward_kwargs: output["pixel_values"] = forward_kwargs["pixel_values"] if "image_grid_thw" in forward_kwargs: output["image_grid_thw"] = forward_kwargs["image_grid_thw"] if "pixel_attention_mask" in forward_kwargs: output["pixel_attention_mask"] = forward_kwargs["pixel_attention_mask"] if "image_sizes" in forward_kwargs: output["image_sizes"] = forward_kwargs["image_sizes"] if "token_type_ids" in forward_kwargs: output["token_type_ids"] = forward_kwargs["token_type_ids"] if images is not None: output["num_images"] = num_images if self.tools: output["tool_mask"] = tool_mask return output def slice_group_data( self, data: torch.Tensor, mask: torch.Tensor, group_idx: int ) -> tuple[torch.Tensor, torch.Tensor]: """ Slices the input data and mask tensors for a specific group index. Also trims the sequence length to the maximum length in the group based on the mask. Args: data: Tensor of shape (num_groups * num_generations, seq_length) mask: Tensor of shape (num_groups * num_generations, seq_length) group_idx: Index of the group to slice Returns: Tuple of (sliced_data, sliced_mask) for the specified group, with sequence length trimmed to the maximum length in the group. """ start_idx = group_idx * self.num_generations end_idx = (group_idx + 1) * self.num_generations group_data = data[start_idx:end_idx] group_mask = mask[start_idx:end_idx] group_max_len = group_mask.sum(dim=1).max().item() return group_data[:, :group_max_len], group_mask[:, :group_max_len] def update_replay_buffer( self, groups_with_variance: torch.Tensor, group_advantages: torch.Tensor, group_std_rewards: torch.Tensor, prompt_ids: torch.Tensor, prompt_mask: torch.Tensor, completion_ids: torch.Tensor, completion_mask: torch.Tensor, forward_kwargs: dict, optional_vision_fields: list[str] = None, old_per_token_logps: torch.Tensor | None = None, ref_per_token_logps: torch.Tensor | None = None, importance_sampling_ratio: float | None = None, ) -> None: """ Update the replay buffer with groups that have reward variance (std > 0). Args: groups_with_variance: Boolean tensor indicating which groups have reward variance group_advantages: Tensor of shape (num_groups, num_generations) containing advantage values std_rewards: Tensor of shape (num_groups, num_generations) containing std of rewards per group prompt_ids: Tensor containing prompt token IDs prompt_mask: Tensor containing prompt attention masks completion_ids: Tensor containing completion token IDs completion_mask: Tensor containing completion attention masks forward_kwargs: Dictionary containing additional prompt inputs (vision data, etc.) optional_vision_fields: List of optional vision-related fields to include if present in forward_kwargs old_per_token_logps: Optional tensor of old per-token log probabilities ref_per_token_logps: Optional tensor of reference per-token log probabilities importance_sampling_ratio: Optional importance sampling correction ratio """ # Prepare buffered outputs for groups with variance buffered_outputs = [] for _, group_idx in enumerate(groups_with_variance.nonzero(as_tuple=True)[0].unique().tolist()): group_prompt_ids, group_prompt_mask = self.slice_group_data(prompt_ids, prompt_mask, group_idx) group_completion_ids, group_completion_mask = self.slice_group_data( completion_ids, completion_mask, group_idx ) # Store unpadded data in the buffer buffered_output = { "prompt_ids": group_prompt_ids, "completion_ids": group_completion_ids, "advantages": group_advantages[group_idx].tolist(), "prompt_mask": group_prompt_mask, "completion_mask": group_completion_mask, } # Add optional fields if they exist optional_fields = { "old_per_token_logps": old_per_token_logps if old_per_token_logps is not None else None, "ref_per_token_logps": ref_per_token_logps if ref_per_token_logps is not None else None, } for field_name, field_data in optional_fields.items(): if field_data is not None: buffered_output[field_name] = self.slice_group_data(field_data, completion_mask, group_idx)[0] # Add importance sampling if needed if self.use_vllm and self.vllm_importance_sampling_correction: buffered_output["importance_sampling_ratio"] = importance_sampling_ratio if optional_vision_fields: # Add vision-related fields if they exist for field_name in optional_vision_fields: if field_name in forward_kwargs: buffered_output[field_name] = self.slice_group_data( forward_kwargs[field_name], prompt_mask, group_idx )[0] buffered_outputs.append(buffered_output) if groups_with_variance.any(): # Calculate replay buffer scores for groups with variance replay_buffer_scores = (group_advantages.abs() * group_std_rewards).sum(dim=-1)[groups_with_variance] # Add all groups to replay buffer at once (batch operation) self.replay_buffer.add(replay_buffer_scores.tolist(), buffered_outputs) def sample_from_replay_buffer( self, num_samples: int, optional_vision_fields: list[str] = None, optional_tensor_fields: list[str] = None ) -> list[dict]: """ Sample groups from the replay buffer. Args: num_samples: Number of samples to draw from the replay buffer optional_vision_fields: List of optional vision-related fields to include if present in sampled data optional_tensor_fields: List of optional tensor fields to include if present in sampled data Returns: List of sampled data dictionaries from the replay buffer """ sampled = self.replay_buffer.sample(num_samples=num_samples) # Extract and concatenate sampled data sampled_data = { "prompt_ids": [], "prompt_mask": [], "completion_ids": [], "completion_mask": [], "advantages": [], } all_optional_fields = (optional_tensor_fields or []) + (optional_vision_fields or []) # Initialize containers for optional fields if they exist in sampled data for field in all_optional_fields: if sampled and field in sampled[0]: sampled_data[field] = [] # Extract data from each sampled item for item in sampled: # Handle core fields for key in ["prompt_ids", "prompt_mask", "completion_ids", "completion_mask"]: sampled_data[key].append(item[key]) # Handle advantages (list, not tensor) sampled_data["advantages"].append(item["advantages"]) # Handle optional fields for field in all_optional_fields: if field in item: sampled_data[field].append(item[field]) return sampled_data def update_with_replay_buffer( self, group_advantages: torch.Tensor, group_std_rewards: torch.Tensor, prompt_ids: torch.Tensor, prompt_mask: torch.Tensor, completion_ids: torch.Tensor, completion_mask: torch.Tensor, forward_kwargs: dict, num_items_in_batch: int, old_per_token_logps: torch.Tensor | None = None, ref_per_token_logps: torch.Tensor | None = None, importance_sampling_ratio: float | None = None, ) -> None: """ Update current batch data with samples from replay buffer. Groups with reward variance (std > 0) are added to the replay buffer and then replaced with samples from the buffer to improve training stability. Args: group_advantages: Tensor of shape (num_groups, num_generations) containing advantage values std_rewards: Tensor of shape (num_groups, num_generations) containing std of rewards per group prompt_ids: Tensor containing prompt token IDs prompt_mask: Tensor containing prompt attention masks completion_ids: Tensor containing completion token IDs completion_mask: Tensor containing completion attention masks forward_kwargs: Dictionary containing additional prompt inputs (vision data, etc.) num_items_in_batch: Number of items in the current batch old_per_token_logps: Optional tensor of old per-token log probabilities ref_per_token_logps: Optional tensor of reference per-token log probabilities importance_sampling_ratio: Optional importance sampling correction ratio """ if self.replay_buffer.max_size <= 0: return # Groups to consider for adding to the replay buffer groups_with_variance = group_std_rewards.max(dim=0).values > 0 # Groups to replace from the replay buffer groups_without_variance = ~groups_with_variance # Track which optional fields are present in sampled data optional_tensor_fields = ["old_per_token_logps", "ref_per_token_logps"] vision_fields = ["pixel_values", "image_grid_thw", "pixel_attention_mask", "image_sizes"] self.update_replay_buffer( groups_with_variance, group_advantages, group_std_rewards, prompt_ids, prompt_mask, completion_ids, completion_mask, forward_kwargs, vision_fields, old_per_token_logps, ref_per_token_logps, importance_sampling_ratio, ) # Sample from replay buffer to replace groups with variance num_groups_to_replace = groups_without_variance.sum().item() if not num_groups_to_replace: return sampled_data = self.sample_from_replay_buffer( num_samples=num_groups_to_replace, optional_vision_fields=vision_fields, optional_tensor_fields=optional_tensor_fields, ) # Pad sampled data if they are shorter than the current batch sequences # Or pad the current batch if sampled are longer current_batch_prompt_seq_len = prompt_ids.size(1) current_batch_completion_seq_len = completion_ids.size(1) groups_to_replace_idxs = groups_with_variance.logical_not().nonzero(as_tuple=True)[0].unique().tolist() # Determine target (max) sequence lengths once sampled_prompt_lengths = [t.size(1) for t in sampled_data["prompt_ids"]] sampled_completion_lengths = [t.size(1) for t in sampled_data["completion_ids"]] target_prompt_len = max([current_batch_prompt_seq_len] + sampled_prompt_lengths) target_completion_len = max([current_batch_completion_seq_len] + sampled_completion_lengths) # If any sampled prompt is longer, pad the whole batch prompt tensors once (left padding) if target_prompt_len > current_batch_prompt_seq_len: prompt_ids = pad( list(prompt_ids.unbind(0)), padding_value=self.pad_token_id, pad_to_multiple_of=target_prompt_len, padding_side="left", ) prompt_mask = pad( list(prompt_mask.unbind(0)), padding_value=0, pad_to_multiple_of=target_prompt_len, padding_side="left" ) # If any sampled completion is longer, pad the whole batch completion tensors once (right padding) if target_completion_len > current_batch_completion_seq_len: completion_ids = pad( list(completion_ids.unbind(0)), padding_value=self.pad_token_id, pad_to_multiple_of=target_completion_len, padding_side="right", ) completion_mask = pad( list(completion_mask.unbind(0)), padding_value=0, pad_to_multiple_of=target_completion_len, padding_side="right", ) if old_per_token_logps is not None: old_per_token_logps = pad( list(old_per_token_logps.unbind(0)), padding_value=0.0, pad_to_multiple_of=target_completion_len, padding_side="right", ) if ref_per_token_logps is not None: ref_per_token_logps = pad( list(ref_per_token_logps.unbind(0)), padding_value=0.0, pad_to_multiple_of=target_completion_len, padding_side="right", ) # Replace per-group data, padding only sampled groups that are shorter than the target for i, group_idx in enumerate(groups_to_replace_idxs): start_idx = group_idx * self.num_generations end_idx = (group_idx + 1) * self.num_generations idx_range = slice(start_idx, end_idx) # Pad sampled prompt to target length if needed if sampled_data["prompt_ids"][i].size(1) < target_prompt_len: sampled_data["prompt_ids"][i] = pad( sampled_data["prompt_ids"][i], padding_value=self.pad_token_id, pad_to_multiple_of=target_prompt_len, padding_side="left", ) sampled_data["prompt_mask"][i] = pad( sampled_data["prompt_mask"][i], padding_value=0, pad_to_multiple_of=target_prompt_len, padding_side="left", ) # Pad sampled completion to target length if needed if sampled_data["completion_ids"][i].size(1) < target_completion_len: sampled_data["completion_ids"][i] = pad( sampled_data["completion_ids"][i], padding_value=self.pad_token_id, pad_to_multiple_of=target_completion_len, padding_side="right", ) sampled_data["completion_mask"][i] = pad( sampled_data["completion_mask"][i], padding_value=0, pad_to_multiple_of=target_completion_len, padding_side="right", ) if "old_per_token_logps" in sampled_data: sampled_data["old_per_token_logps"][i] = pad( sampled_data["old_per_token_logps"][i], padding_value=0.0, pad_to_multiple_of=target_completion_len, padding_side="right", ) if "ref_per_token_logps" in sampled_data: sampled_data["ref_per_token_logps"][i] = pad( sampled_data["ref_per_token_logps"][i], padding_value=0.0, pad_to_multiple_of=target_completion_len, padding_side="right", ) # Assign (replace) group slice prompt_ids[idx_range] = sampled_data["prompt_ids"][i] prompt_mask[idx_range] = sampled_data["prompt_mask"][i] completion_ids[idx_range] = sampled_data["completion_ids"][i] completion_mask[idx_range] = sampled_data["completion_mask"][i] group_advantages[group_idx] = sampled_data["advantages"][i] if "old_per_token_logps" in sampled_data: old_per_token_logps[idx_range] = sampled_data["old_per_token_logps"][i] if "ref_per_token_logps" in sampled_data: ref_per_token_logps[idx_range] = sampled_data["ref_per_token_logps"][i] for field in vision_fields: if field in sampled_data and field in forward_kwargs: forward_kwargs[field][idx_range] = sampled_data[field][i] # Prepare final outputs after sampling and replacement outputs_after_sampling_buffer = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "advantages": group_advantages, } # Replace optional tensor fields if they exist for field in optional_tensor_fields: if field in sampled_data: outputs_after_sampling_buffer[field] = ( old_per_token_logps if field == "old_per_token_logps" else ref_per_token_logps ) # Replace vision fields if they exist for field in vision_fields: if field in sampled_data and field in forward_kwargs: outputs_after_sampling_buffer[field] = forward_kwargs[field] outputs_after_sampling_buffer["num_items_in_batch"] = num_items_in_batch if self.use_vllm and self.vllm_importance_sampling_correction: outputs_after_sampling_buffer["importance_sampling_ratio"] = importance_sampling_ratio return outputs_after_sampling_buffer ================================================ FILE: trl/experimental/gspo_token/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .grpo_trainer import GRPOTrainer ================================================ FILE: trl/experimental/gspo_token/grpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import torch from ...trainer.grpo_trainer import GRPOTrainer as _GRPOTrainer from ...trainer.utils import nanmax, nanmin class GRPOTrainer(_GRPOTrainer): def _compute_loss(self, model, inputs): # Compute the per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens # Compute the per_token_logps and the entropy at each position in the completion per_token_logps, entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, pixel_values=inputs.get("pixel_values"), image_grid_thw=inputs.get("image_grid_thw"), num_images=inputs.get("num_images"), pixel_attention_mask=inputs.get("pixel_attention_mask"), image_sizes=inputs.get("image_sizes"), token_type_ids=inputs.get("token_type_ids"), ) if self.top_entropy_quantile < 1.0: entropy_mask = self.get_high_entropy_mask(entropies, completion_mask, 1 - self.top_entropy_quantile) else: entropy_mask = None # Compute the KL divergence between the model and the reference model if self.beta != 0.0: ref_per_token_logps = inputs["ref_per_token_logps"] per_token_kl = ( torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1 ) # Compute the loss advantages = inputs["advantages"] # When num_iterations == 1 and steps_per_generation <= gradient_accumulation_steps, # old_per_token_logps == per_token_logps. In this case we can skip its computation # (see _generate_and_score_completions) and instead use per_token_logps.detach(). # The exception is when using vLLM, where we always compute old_per_token_logps # for importance sampling old_per_token_logps = inputs.get("old_per_token_logps") old_per_token_logps = per_token_logps.detach() if old_per_token_logps is None else old_per_token_logps log_ratio = per_token_logps - old_per_token_logps if self.importance_sampling_level == "token": log_importance_weights = log_ratio elif self.importance_sampling_level == "sequence": log_importance_weights = (log_ratio * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) log_importance_weights = log_importance_weights.unsqueeze(-1) elif self.importance_sampling_level == "sequence_token": # GSPO-token: sg[si(θ)] * πθ(yi,t)/sg[πθ(yi,t)] seq_level_log_weight = (log_ratio * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) seq_level_log_weight = seq_level_log_weight.detach().unsqueeze(-1) # Stop gradient log_importance_weights = per_token_logps - per_token_logps.detach() + seq_level_log_weight else: raise ValueError( f"Unknown importance sampling level: {self.importance_sampling_level}. Possible values are 'token' " "and 'sequence'." ) # From here, log_importance_weights (and all subsequent tensors, coef_1, coef_2, etc.) shape depends on # importance_sampling_level: "token" level: (B, T); "sequence" level: (B, 1) coef_1 = torch.exp(log_importance_weights) coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high) # Two-sided clipping if self.args.delta is not None: coef_1 = torch.clamp(coef_1, max=self.args.delta) per_token_loss1 = coef_1 * advantages.unsqueeze(1) per_token_loss2 = coef_2 * advantages.unsqueeze(1) per_token_loss = -torch.min(per_token_loss1, per_token_loss2) if entropy_mask is not None: per_token_loss = per_token_loss * entropy_mask if self.use_vllm and self.vllm_importance_sampling_correction: per_token_loss = per_token_loss * inputs["importance_sampling_ratio"] if self.beta != 0.0: per_token_loss = per_token_loss + self.beta * per_token_kl mode = "train" if self.model.training else "eval" if self.loss_type == "grpo": loss = ((per_token_loss * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0)).mean() normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type == "bnpo": loss = (per_token_loss * completion_mask).sum() / completion_mask.sum().clamp(min=1.0) normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type == "dr_grpo": loss = (per_token_loss * completion_mask).sum() / (per_token_loss.size(0) * self.max_completion_length) normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type == "dapo": normalizer = inputs["num_items_in_batch"] / self.accelerator.num_processes loss = (per_token_loss * completion_mask).sum() / normalizer else: raise ValueError(f"Unknown loss type: {self.loss_type}") # Log the metrics completion_token_count = completion_mask.sum().clamp(min=1.0) def masked_batch_mean(x): if x.shape[1] == 1: # when importance_sampling_level == "sequence" return x.mean() else: return (x * completion_mask).sum() / completion_token_count if self.beta != 0.0: mean_kl = masked_batch_mean(per_token_kl) self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).nanmean().item()) mean_entropy = masked_batch_mean(entropies) self._metrics[mode]["entropy"].append(self.accelerator.gather(mean_entropy).nanmean().item()) # Compute the clipped probability ratios is_low_clipped = (coef_1 < 1 - self.epsilon_low) & (advantages.unsqueeze(1) < 0) is_high_clipped = (coef_1 > 1 + self.epsilon_high) & (advantages.unsqueeze(1) > 0) is_region_clipped = is_low_clipped | is_high_clipped low_clip = masked_batch_mean(is_low_clipped.float()) high_clip = masked_batch_mean(is_high_clipped.float()) clip_ratio = masked_batch_mean(is_region_clipped.float()) gathered_low_clip = self.accelerator.gather(low_clip) self._metrics[mode]["clip_ratio/low_mean"].append(gathered_low_clip.nanmean().item()) self._metrics[mode]["clip_ratio/low_min"].append(nanmin(gathered_low_clip).item()) gathered_high_clip = self.accelerator.gather(high_clip) self._metrics[mode]["clip_ratio/high_mean"].append(gathered_high_clip.nanmean().item()) self._metrics[mode]["clip_ratio/high_max"].append(nanmax(gathered_high_clip).item()) gathered_clip_ratio = self.accelerator.gather(clip_ratio) self._metrics[mode]["clip_ratio/region_mean"].append(gathered_clip_ratio.nanmean().item()) return loss ================================================ FILE: trl/experimental/judges/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .judges import ( AllTrueJudge, BaseBinaryJudge, BaseJudge, BasePairwiseJudge, BaseRankJudge, HfPairwiseJudge, OpenAIPairwiseJudge, PairRMJudge, ) __all__ = [ "AllTrueJudge", "BaseBinaryJudge", "BaseJudge", "BasePairwiseJudge", "BaseRankJudge", "HfPairwiseJudge", "OpenAIPairwiseJudge", "PairRMJudge", ] ================================================ FILE: trl/experimental/judges/judges.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import concurrent.futures import logging from abc import ABC, abstractmethod import numpy as np from accelerate import Accelerator from huggingface_hub import InferenceClient from packaging.version import Version from transformers.utils import is_openai_available from ...import_utils import is_llm_blender_available DEFAULT_PAIRWISE_SYSTEM_PROMPT = '''I require a leaderboard for various large language models. I'll provide you with prompts given to these models and their corresponding outputs. Your task is to assess these responses, and select the model that produces the best output from a human perspective. ## Instruction {{ "instruction": """{prompt}""", }} ## Model Outputs Here are the unordered outputs from the models. Each output is associated with a specific model, identified by a unique model identifier. {{ {{ "model_identifier": "0", "output": """{response0}""" }}, {{ "model_identifier": "1", "output": """{response1}""" }} }} ## Task Evaluate the models on the basis of the quality and relevance of their results, and select the model that generated the best result. Reply with the identifier of the best model. Our evaluation will only take into account the first character of your answer, so make sure it contains only one of the identifiers and nothing else (no quotation marks, no spaces, no new lines, ...). ''' def _ensure_llm_blender_importable() -> None: """ Pre-import shim to work around a known `llm-blender` issue. As of `llm-blender` v0.0.2 (see upstream issue: https://github.com/yuchenlin/LLM-Blender/issues/33), importing `llm_blender` may fail on `transformers` >= 5.0.0 because it unconditionally accesses `transformers.utils.hub.TRANSFORMERS_CACHE`. We set this attribute to a dummy value before importing `llm_blender` so that the import succeeds. This helper is intentionally a no-op on older `transformers` versions. This shim can be removed once the upstream issue is fixed and the minimum required `llm-blender` version includes that fix. """ import transformers.utils.hub if Version(transformers.__version__) >= Version("5.0.0"): transformers.utils.hub.TRANSFORMERS_CACHE = None # unused; just needs to exist class BaseJudge(ABC): """ Base class for judges. The subclasses of this class should implement the `judge` method. """ @abstractmethod def judge(self, prompts: list[str], completions: list[str], shuffle_order: bool = True) -> list: raise NotImplementedError("Judge subclasses must implement the `judge` method.") class BaseRankJudge(ABC): """ Base class for LLM ranking judges. **Example**: ```python class MyRankJudge(BaseRankJudge): def judge(self, prompts, completions, shuffle_order=True): return ... # Your ranking logic here judge = MyRankJudge() judge.judge( prompts=["The capital of France is", "The capital of Germany is"], completions=[[" Paris", " Marseille", "Lyon"], [" Munich", " Berlin"]], ) # [[0, 1, 2], [1, 0]] ``` """ @abstractmethod def judge(self, prompts: list[str], completions: list[list[str]], shuffle_order: bool = True) -> list[list[int]]: """ Judge the completion for the given prompts and return the ranks of each completion. Args: prompts (`list[str]`): List of prompts. completions (`list[list[str]]`): List of completions list, where each element is a list of completions for the corresponding prompt. shuffle_order (`bool`, *optional*, defaults to `True`): Whether to shuffle the order of the completions to avoid positional bias. Returns: `list[list[int]]`: List of lists of idxs, where each list contains the ranks of the completions for the corresponding prompt. E.g., `[1, 2, 0]` means that the second completion (`idx=1`) is the best, followed by the third, and then the first. """ raise NotImplementedError("Judge subclasses must implement the `judge` method.") class BasePairwiseJudge(BaseJudge): """ Base class for pairwise judges. """ @abstractmethod def judge(self, prompts: list[str], completions: list[list[str]], shuffle_order: bool = True) -> list[int]: """ Judge the completion pairs for the given prompts. Args: prompts (`list[str]`): List of prompts. completions (`list[list[str]]`): List of completions pairs, where each element is a pair of completions for the corresponding prompt. shuffle_order (`bool`, *optional*, defaults to `True`): Whether to shuffle the order of the completions to avoid positional bias. Returns: `list[int]`: List of idxs, where each idx is the rank of the best completion for the corresponding prompt. E.g., `1` means that the second completion (`idx=1`) is the best. Note: If the judge returns `-1` for any prompt, it indicates that the inner process used to compute the preference has failed. For instance, this could occur if the underlying language model returned an invalid answer. In such cases, the caller should handle these invalid indices appropriately, possibly by implementing fallback logic or error handling. """ raise NotImplementedError("Judge subclasses must implement the `judge` method.") class BaseBinaryJudge(BaseJudge): """ Base class for binary judges. """ @abstractmethod def judge( self, prompts: list[str], completions: list[str], gold_completions: list[str] | None = None, shuffle_order: bool = True, ) -> list[int]: """ Judge the completion for a given prompt. Used to assess if a completion satisfies a constraint. This base class should be used to implement binary evaluations as done in section 4.1.4 of the [CGPO paper](https://huggingface.co/papers/2409.20370). It is relevant for assessing whether a prompt-completion pair satisfies a specific constraint. Args: prompts (`list[str]`): List of prompts. completions (`list[str]`): List of completions. gold_completions (`list[str]`, `optional`): List of gold completions if it exists. shuffle_order (`bool`): Whether to shuffle the order of the completions to avoid positional bias. Returns: list[int]: A list of binary labels: - 1 indicates that the completion satisfies the evaluated constraint. - 0 indicates that the completion does not satisfy the evaluated constraint. Note: If the judge returns -1 for any prompt, it indicates that the inner process used to compute the preference has failed. For instance, this could occur if the underlying language model or rule based constraint returned an invalid answer. In such cases, the caller should handle these invalid indices appropriately, possibly by implementing fallback logic or error handling. """ raise NotImplementedError("Judge subclasses must implement the `judge` method.") class PairRMJudge(BasePairwiseJudge): # docstyle-ignore """ LLM judge based on the PairRM model from AllenAI. This judge uses the PairRM model to rank pairs of completions for given prompts. It's designed for pairwise comparison of language model outputs. The PairRM model is loaded using the llm-blender library and runs on the default Accelerator device. **Attributes**: blender (`llm_blender.Blender`): An instance of the Blender class from llm-blender. **Example**: ```python >>> pairrm_judge = PairRMJudge() >>> prompts = ["Translate 'hello' to French", "What's the capital of Japan?"] >>> completions = [["Bonjour", "Salut"], ["Kyoto", "Tokyo"]] >>> results = pairrm_judge.judge(prompts, completions) >>> print(results) # [0, 1] (indicating the first completion is preferred for the first prompt and the second) ``` > [!TIP] > This class requires the llm-blender library to be installed. Install it with: `pip install llm-blender`. """ def __init__(self): if not is_llm_blender_available(): raise ValueError("llm-blender is not installed. Please install it with `pip install llm-blender`.") import transformers if Version(transformers.__version__) >= Version("5.0.0"): raise RuntimeError( "llm-blender currently supports transformers < 5.0.0. Please install a compatible version: `pip install 'transformers<5.0.0'`. Check the issue tracker for updates: https://github.com/huggingface/trl/issues/4918" ) _ensure_llm_blender_importable() import llm_blender self.blender = llm_blender.Blender() self.blender.loadranker("llm-blender/PairRM", device=Accelerator().device) def judge( self, prompts: list[str], completions: list[list[str]], shuffle_order: bool = True, return_scores: bool = False, temperature: float = 1.0, ) -> list[int | float]: """ Judge the completion pairs for the given prompts using the PairRM model. Args: prompts (`list[str]`): List of prompts to judge. completions (`list[list[str]]`): List of completion pairs for each prompt. shuffle_order (`bool`, *optional*, defaults to `True`): Whether to shuffle the order of the completions to avoid positional bias. return_scores (`bool`, *optional*, defaults to `False`): If `True`, return probability scores of the first completion instead of ranks (i.e. a *soft-judge*). temperature (`float`, *optional*, defaults to `1.0`): Temperature for scaling logits if `return_scores` is True. Returns: `list[int | float]`: If `return_scores` is `False`, returns a list of ranks (`0` or `1`) for each prompt, indicating which completion is preferred. If `return_scores` is `True`, returns softmax probabilities for the first completion. Raises: `ValueError`: If the number of completions per prompt is not exactly 2. Note: Unlike llm-blender, ranks are 0-indexed (`0` means the first completion is preferred). """ if len(completions[0]) != 2: raise ValueError("PairRM judge requires exactly 2 completions per prompt.") # Shuffle the order of the completions to avoid positional bias if shuffle_order: flip_mask = np.random.choice([True, False], size=len(prompts)) completions = [pair[::-1] if flip else pair for flip, pair in zip(flip_mask, completions, strict=True)] # Rank the completions ranks = self.blender.rank(prompts, completions, return_scores=return_scores, disable_tqdm=True) if not return_scores: ranks -= 1 # PairRM rank is 1-indexed, so we subtract 1 to make it 0-indexed else: # scale the logits by temperature ranks /= temperature # Flip back the ranks or scores to the original order if needed if shuffle_order: ranks[flip_mask] = ranks[flip_mask][:, ::-1] # Return the ranks or score probability if return_scores: logit_max = np.amax(ranks, axis=-1, keepdims=True) exp_logit_shifted = np.exp(ranks - logit_max) probs = exp_logit_shifted / np.sum(exp_logit_shifted, axis=-1, keepdims=True) return probs[:, 0].tolist() else: return ranks[:, 0].tolist() class HfPairwiseJudge(BasePairwiseJudge): """ Pairwise judge based on the Hugging Face API with chat completion. This judge is relevant for assessing the quality chat models, where the completion is a response to a given prompt. Args: model (`str`, *optional*, defaults to `"meta-llama/Meta-Llama-3-70B-Instruct"`): Model to use for the judge. token (`str`, *optional*): Hugging Face API token to use for the [`huggingface_hub.InferenceClient`]. system_prompt (`str`, *optional*): The system prompt to be used for the judge. If not provided, a default prompt is used. Note that the system prompt should contain the following placeholders: `{prompt}`, `{response0}`, and `{response1}`. Also, the inference is called with `max_tokens=1`, consequently the system prompt should ask for a single token response. """ def __init__( self, model="meta-llama/Meta-Llama-3-70B-Instruct", token: str | None = None, system_prompt: str | None = None, ): self.client = InferenceClient(model=model, token=token) self.system_prompt = system_prompt or DEFAULT_PAIRWISE_SYSTEM_PROMPT def judge(self, prompts: list[str], completions: list[list[str]], shuffle_order: bool = True) -> list[int]: # Shuffle the order of the completions to avoid positional bias if shuffle_order: flip_mask = np.random.choice([True, False], size=len(prompts)) completions = [pair[::-1] if flip else pair for flip, pair in zip(flip_mask, completions, strict=True)] # Define a function to get the rank for a single prompt, will be called concurrently def get_rank(prompt, candidates): content = self.system_prompt.format(prompt=prompt, response0=candidates[0], response1=candidates[1]) completion = self.client.chat_completion(messages=[{"role": "user", "content": content}], max_tokens=1) response = completion.choices[0].message.content if response in ["0", "1"]: return int(response) else: logging.debug(f"Invalid response from the judge model: '{response}'. Returning -1.") return -1 # Call the completions concurrently with concurrent.futures.ThreadPoolExecutor() as executor: ranks = list(executor.map(get_rank, prompts, completions)) # Flip back the ranks to the original order if needed if shuffle_order: ranks = [ranks[i] if not flip else 1 - ranks[i] for i, flip in enumerate(flip_mask)] # Return the ranks return ranks class OpenAIPairwiseJudge(BasePairwiseJudge): """ Judge based on the OpenAI API. This judge is relevant for assessing the quality chat models, where the completion is a response to a given prompt. Args: model (`str`, *optional*, defaults to `"gpt-4-turbo-preview"`): Model to use for the judge. system_prompt (`str`, *optional*): System prompt to be used for the judge. If not provided, a default prompt is used. Note that the system prompt should contain the following placeholders: `{prompt}`, `{response0}`, and `{response1}`. Also, the inference is called with `max_tokens=1`, consequently the system prompt should ask for a single token response. max_requests (`int` or `None`, *optional*, defaults to `1000`): Maximum number of requests to make to the OpenAI API. If set to `None`, there is no limit. """ def __init__( self, model="gpt-4-turbo-preview", system_prompt: str | None = None, max_requests: int | None = 1_000 ): if not is_openai_available(): raise ValueError("OpenAI client is not installed. Please install it with 'pip install openai'.") from openai import OpenAI self.client = OpenAI() self.model = model self.system_prompt = system_prompt or DEFAULT_PAIRWISE_SYSTEM_PROMPT self.max_requests = max_requests self.num_requests = 0 self._warned = False def judge(self, prompts: list[str], completions: list[list[str]], shuffle_order: bool = True) -> list[int]: # Check if the limit of requests is reached, if so, use random choice instead if self.max_requests is not None and self.num_requests >= self.max_requests: if not self._warned: # Print the warning only once logging.warning( f"Reached the maximum number of requests ({self.max_requests}). From now on, returning -1 instead. " " To increase the limit, set `max_requests` to a higher value, or to `None` for no limit." ) self._warned = True return [-1] * len(prompts) # Shuffle the order of the completions to avoid positional bias if shuffle_order: flip_mask = np.random.choice([True, False], size=len(prompts)) completions = [pair[::-1] if flip else pair for flip, pair in zip(flip_mask, completions, strict=True)] # Define a function to get the rank for a single prompt, will be called concurrently def get_rank(prompt, candidates): content = self.system_prompt.format(prompt=prompt, response0=candidates[0], response1=candidates[1]) messages = [{"role": "user", "content": content}] completion = self.client.chat.completions.create(model=self.model, messages=messages, max_tokens=1) response = completion.choices[0].message.content if response in ["0", "1"]: return int(response) else: logging.debug(f"Invalid response from the judge model: '{response}'. Returning -1.") return -1 # Call the completions concurrently with concurrent.futures.ThreadPoolExecutor() as executor: ranks = list(executor.map(get_rank, prompts, completions)) # Flip back the ranks to the original order if needed if shuffle_order: ranks = [ranks[i] if not flip else 1 - ranks[i] for i, flip in enumerate(flip_mask)] # Update the number of requests self.num_requests += len(prompts) # Return the ranks return ranks class AllTrueJudge(BaseBinaryJudge): """ Unify the decision of multiple [`experimental.judges.BaseBinaryJudge`] instances. Returns `1` only if all inner binary judges return `1`. If any judge returns `0`, it returns `0`. If any judge returns `-1`, indicating a failure in its process, this judge will also return `-1`. Implements the Mixture of Judges as described in the [CGPO paper](https://huggingface.co/papers/2409.20370). Args: judges (`list` of [`experimental.judges.BaseBinaryJudge`]): A list of [`experimental.judges.BaseBinaryJudge`] instances whose decisions will be unified. """ def __init__(self, judges: list[BaseBinaryJudge]): self.judges = judges def judge( self, prompts: list[str], completions: list[str], gold_completions: list[str] | None = None, shuffle_order: bool = True, ) -> list[int]: all_binary_judgments = [ judge.judge(prompts, completions, gold_completions, shuffle_order) for judge in self.judges ] output = [] for binary_judgments in zip(*all_binary_judgments, strict=True): # Check that all values are in {0, 1, -1} if any(binary_judgment not in {0, 1, -1} for binary_judgment in binary_judgments): raise ValueError( f"Invalid binary judgment: {binary_judgments}, expected list of values in {{0, 1, -1}}." ) # Unify the decision if -1 in binary_judgments: output.append(-1) elif all(binary_judgment == 1 for binary_judgment in binary_judgments): output.append(1) else: output.append(0) return output ================================================ FILE: trl/experimental/kto/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .kto_config import KTOConfig from .kto_trainer import KTOTrainer __all__ = ["KTOConfig", "KTOTrainer"] ================================================ FILE: trl/experimental/kto/kto_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from ...trainer.base_config import _BaseConfig @dataclass class KTOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.kto.KTOTrainer`]. This class includes only the parameters that are specific to KTO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the sequences (prompt + completion) in the batch. This argument is required if you want to use the default data collator. beta (`float`, *optional*, defaults to `0.1`): Parameter controlling the deviation from the reference model. Higher β means less deviation from the reference model. loss_type (`str`, *optional*, defaults to `"kto"`): Type of loss to use. Possible values are: - `"kto"`: KTO loss from the [KTO](https://huggingface.co/papers/2402.01306) paper. - `"apo_zero_unpaired"`: Unpaired variant of APO-zero loss from the [APO](https://huggingface.co/papers/2408.06266) paper. desirable_weight (`float`, *optional*, defaults to `1.0`): Desirable losses are weighed by this factor to counter unequal number of desirable and undesirable paris. undesirable_weight (`float`, *optional*, defaults to `1.0`): Undesirable losses are weighed by this factor to counter unequal number of desirable and undesirable pairs. generate_during_eval (`bool`, *optional*, defaults to `False`): If `True`, generates and logs completions from both the model and the reference model to W&B or Comet during evaluation. precompute_ref_log_probs (`bool`, *optional*, defaults to `False`): Whether to precompute reference model log probabilities for training and evaluation datasets. This is useful when training without the reference model to reduce the total GPU memory needed. model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model from a string. dataset_num_proc: (`int`, *optional*): Number of processes to use for processing the dataset. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model and reference model. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) max_length: int | None = field( default=1024, metadata={"help": "Maximum length of the sequences (prompt + completion) in the batch."}, ) beta: float = field( default=0.1, metadata={ "help": "Parameter controlling the deviation from the reference model. Higher β means less deviation from " "the reference model." }, ) loss_type: str = field( default="kto", metadata={ "help": "Type of loss to use.", "choices": ["kto", "apo_zero_unpaired"], }, ) desirable_weight: float = field( default=1.0, metadata={ "help": "Desirable losses are weighed by this factor to counter unequal number of desirable and " "undesirable pairs.", }, ) undesirable_weight: float = field( default=1.0, metadata={ "help": "Undesirable losses are weighed by this factor to counter unequal number of desirable and " "undesirable pairs.", }, ) generate_during_eval: bool = field( default=False, metadata={ "help": "If `True`, generates and logs completions from both the model and the reference model to W&B " "during evaluation." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model."}, ) precompute_ref_log_probs: bool = field( default=False, metadata={ "help": "Whether to precompute reference model log probabilities for training and evaluation datasets. " "This is useful when training without the reference model to reduce the total GPU memory needed." }, ) model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model " "from a string." }, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) def __post_init__(self): self.bf16 = not (self.fp16) if self.bf16 is None else self.bf16 super().__post_init__() ================================================ FILE: trl/experimental/kto/kto_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import random import textwrap from collections import defaultdict from collections.abc import Callable from contextlib import contextmanager, nullcontext from operator import itemgetter from pathlib import Path from typing import TYPE_CHECKING, Any, Literal import numpy as np import pandas as pd import torch import torch.nn as nn import torch.nn.functional as F import transformers from accelerate import PartialState, logging from accelerate.utils import tqdm from datasets import Dataset, concatenate_datasets from packaging.version import Version from torch import autocast from torch.utils.data import DataLoader, SequentialSampler from transformers import ( BaseImageProcessor, DataCollator, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, TrainingArguments, is_comet_available, is_wandb_available, ) from transformers.trainer_utils import EvalLoopOutput, has_length from transformers.utils import is_peft_available from ...data_utils import maybe_apply_chat_template, maybe_extract_prompt, maybe_unpair_preference_dataset from ...import_utils import is_liger_kernel_available from ...models.utils import prepare_deepspeed from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import ( create_model_from_path, disable_dropout_in_model, log_table_to_comet_experiment, selective_log_softmax, ) from ..utils import DPODataCollatorWithPadding, create_reference_model, pad_to_length, peft_module_casting_to_bf16 from .kto_config import KTOConfig if is_liger_kernel_available(): from liger_kernel.chunked_loss import LigerFusedLinearKTOLoss if is_peft_available(): from peft import PeftModel, get_peft_model, prepare_model_for_kbit_training if is_wandb_available(): import wandb if TYPE_CHECKING: from transformers import PreTrainedModel, PreTrainedTokenizer logger = logging.get_logger(__name__) RUNNING_NAME = "running.pt" def _get_kl_dataset(batch: dict[str, list[Any]]) -> dict[str, list[Any]]: """ Creates mismatched pairs of prompts and completions for the KL dataset by adding a +1 offset to the order of completions. For best results, the mismatched outputs y' used to estimate the KL term for a batch should be the same set as the matched outputs y used to estimate the rewards in that batch, just paired with different x. """ batch["answer_input_ids"] = [batch["answer_input_ids"][-1]] + batch["answer_input_ids"][:-1] batch["answer_attention_mask"] = [batch["answer_attention_mask"][-1]] + batch["answer_attention_mask"][:-1] return batch def _tokenize( batch: dict[str, list[Any]], tokenizer: "PreTrainedTokenizer", ) -> dict[str, list[Any]]: """Tokenize a batch from a KTO specific dataset.""" prompt_tokenized = tokenizer(batch["prompt"], add_special_tokens=False) prompt_input_ids = prompt_tokenized["input_ids"] prompt_attention_mask = prompt_tokenized["attention_mask"] prompt_and_completion = [ prompt + completion for prompt, completion in zip(batch["prompt"], batch["completion"], strict=True) ] full_tokenized = tokenizer(prompt_and_completion, add_special_tokens=False) full_input_ids = full_tokenized["input_ids"] full_attention_mask = full_tokenized["attention_mask"] answer_input_ids = [f[len(p) :] for f, p in zip(full_input_ids, prompt_input_ids, strict=True)] answer_attention_mask = [f[len(p) :] for f, p in zip(full_attention_mask, prompt_attention_mask, strict=True)] # Concat tokens to form `enc(a) + enc(a + b)[len(enc(a)):]` full_concat_input_ids = [np.concatenate([p, a]) for p, a in zip(prompt_input_ids, answer_input_ids, strict=True)] # Prepare input tokens for token by token comparison full_input_ids = [np.array(f) for f in full_input_ids] for full, concat in zip(full_input_ids, full_concat_input_ids, strict=True): if len(full) != len(concat): raise ValueError( "The elements in 'full_input_ids' and 'full_concat_input_ids' must have the same pairwise length." ) # On some tokenizers, like Llama-2 tokenizer, there are occasions where tokens # can be merged together when tokenizing prompt+answer. This could result # on the last token from the prompt being different when tokenized on its own # vs when done as prompt+answer. response_token_ids_start_idx = [len(p) for p in prompt_input_ids] # If tokenized prompt is different than both prompt+answer, then it means the # last token has changed due to merging. for idx, (p, f, r) in enumerate(zip(prompt_input_ids, full_input_ids, response_token_ids_start_idx, strict=True)): if not np.array_equal(p, f[:r]): response_token_ids_start_idx[idx] -= 1 prompt_input_ids = [f[:r] for f, r in zip(full_input_ids, response_token_ids_start_idx, strict=True)] prompt_attention_mask = [f[:r] for f, r in zip(full_attention_mask, response_token_ids_start_idx, strict=True)] for p, m in zip(prompt_input_ids, prompt_attention_mask, strict=True): if len(p) != len(m): raise ValueError("Prompt input ids and attention mask should have the same length.") answer_input_ids = [f[r:] for f, r in zip(full_input_ids, response_token_ids_start_idx, strict=True)] answer_attention_mask = [f[r:] for f, r in zip(full_attention_mask, response_token_ids_start_idx, strict=True)] output = dict( prompt_input_ids=prompt_input_ids, prompt_attention_mask=prompt_attention_mask, answer_input_ids=answer_input_ids, answer_attention_mask=answer_attention_mask, ) return output def _process_tokens(example: dict[str, Any], model: "PreTrainedModel" = None, **kwargs) -> dict: """Process tokens of a KTO specific dataset. At this stage, we don't convert to PyTorch tensors yet; we just handle the truncation in case the prompt + completion responses is/are too long. We truncate from the end (completion) to fit within max_length. We also create the labels for the completion responses, which are of length equal to the sum of the length of the prompt and the completion response, with `-100` for the prompt tokens. """ prompt = example["prompt"] completion = example["completion"] batch = { f"{kwargs['prefix']}prompt": prompt, f"{kwargs['prefix']}completion": completion, f"{kwargs['prefix']}label": example["label"], } # Check issues below for more details # 1. https://github.com/huggingface/trl/issues/907 # 2. https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 # 3. https://github.com/LianjiaTech/BELLE/issues/337 if not isinstance(prompt, str): raise ValueError(f"prompt should be an str but got {type(prompt)}") if not isinstance(completion, str): raise ValueError(f"completion should be an str but got {type(completion)}") # keys of format prompt_* refers to just the prompt and answer_* refers to just the answer all_tokens = { "prompt_input_ids": example["prompt_input_ids"], "prompt_attention_mask": example["prompt_attention_mask"], "answer_input_ids": example["answer_input_ids"], "answer_attention_mask": example["answer_attention_mask"], } # calculate max length by checking if BOS/EOS is already there max_length = kwargs["max_length"] bos_token_id = kwargs["tokenizer"].bos_token_id eos_token_id = kwargs["tokenizer"].eos_token_id if len(all_tokens["prompt_input_ids"]) > 0 and bos_token_id != all_tokens["prompt_input_ids"][0]: max_length -= 1 if len(all_tokens["answer_input_ids"]) > 0 and eos_token_id != all_tokens["answer_input_ids"][-1]: max_length -= 1 # if combined sequence is too long, truncate the completion (answer) from the end prompt_length = len(all_tokens["prompt_input_ids"]) completion_length = len(all_tokens["answer_input_ids"]) if prompt_length + completion_length > max_length: max_completion_length = max_length - prompt_length for k in ["answer_input_ids", "answer_attention_mask"]: all_tokens[k] = all_tokens[k][:max_completion_length] # all input_ids and attention mask as is. We then check if we need to add BOS/EOS tokens batch[f"{kwargs['prefix']}prompt_input_ids"] = all_tokens["prompt_input_ids"] batch[f"{kwargs['prefix']}prompt_attention_mask"] = all_tokens["prompt_attention_mask"] batch[f"{kwargs['prefix']}completion_input_ids"] = all_tokens["prompt_input_ids"] + all_tokens["answer_input_ids"] batch[f"{kwargs['prefix']}completion_attention_mask"] = ( all_tokens["prompt_attention_mask"] + all_tokens["answer_attention_mask"] ) # add BOS, which affects both prompt and the full completion if bos_token_id is not None: if len(all_tokens["prompt_input_ids"]) == 0 or bos_token_id != all_tokens["prompt_input_ids"][0]: batch[f"{kwargs['prefix']}prompt_input_ids"] = [bos_token_id] + batch[ f"{kwargs['prefix']}prompt_input_ids" ] batch[f"{kwargs['prefix']}prompt_attention_mask"] = [1] + batch[f"{kwargs['prefix']}prompt_attention_mask"] batch[f"{kwargs['prefix']}completion_input_ids"] = [bos_token_id] + batch[ f"{kwargs['prefix']}completion_input_ids" ] batch[f"{kwargs['prefix']}completion_attention_mask"] = [1] + batch[ f"{kwargs['prefix']}completion_attention_mask" ] # add EOS, which affects only the full completion if len(all_tokens["answer_input_ids"]) == 0 or eos_token_id != all_tokens["answer_input_ids"][-1]: batch[f"{kwargs['prefix']}completion_input_ids"] = batch[f"{kwargs['prefix']}completion_input_ids"] + [ eos_token_id ] batch[f"{kwargs['prefix']}completion_attention_mask"] = batch[ f"{kwargs['prefix']}completion_attention_mask" ] + [1] batch[f"{kwargs['prefix']}completion_labels"] = batch[f"{kwargs['prefix']}completion_input_ids"][:] batch[f"{kwargs['prefix']}completion_labels"][: len(batch[f"{kwargs['prefix']}prompt_input_ids"])] = [-100] * len( batch[f"{kwargs['prefix']}prompt_input_ids"] ) return batch class KTOTrainer(_BaseTrainer): r""" Initialize KTOTrainer. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an [`~transformers.AutoModelForSequenceClassification`]. ref_model ([`~transformers.PreTrainedModel`]): Hugging Face transformer model with a casual language modelling head. Used for implicit reward computation and loss. If no reference model is provided, the trainer will create a reference model with the same architecture as the model to be optimized. args ([`experimental.kto.KTOConfig`]): The arguments to use for training. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. data_collator ([`~transformers.DataCollator`], *optional*): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. model_init (`Callable[[], transformers.PreTrainedModel]`): The model initializer to use for training. If None is specified, the default model initializer will be used. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. peft_config (`dict`, defaults to `None`): The PEFT configuration to use for training. If you pass a PEFT configuration, the model will be wrapped in a PEFT model. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. model_adapter_name (`str`, defaults to `None`): Name of the train target PEFT adapter, when using LoRA with multiple adapters. ref_adapter_name (`str`, defaults to `None`): Name of the reference PEFT adapter, when using LoRA with multiple adapters. """ _tag_names = ["trl", "kto"] _name = "KTO" _paper = { "title": "KTO: Model Alignment as Prospect Theoretic Optimization", "id": "2402.01306", # docstyle-ignore "citation": textwrap.dedent("""\ @article{ethayarajh2024kto, title = {{KTO: Model Alignment as Prospect Theoretic Optimization}}, author = {Kawin Ethayarajh and Winnie Xu and Niklas Muennighoff and Dan Jurafsky and Douwe Kiela}, year = 2024, eprint = {arXiv:2402.01306}, }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str = None, ref_model: PreTrainedModel | nn.Module | str | None = None, args: KTOConfig = None, train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, data_collator: DataCollator | None = None, model_init: Callable[[], PreTrainedModel] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalLoopOutput], dict] | None = None, model_adapter_name: str | None = None, ref_adapter_name: str | None = None, ): if type(args) is TrainingArguments: raise ValueError("Please use `KTOConfig` instead TrainingArguments.") if train_dataset is None: raise ValueError("`train_dataset` is required") if not isinstance(model, str) and ref_model is model: raise ValueError( "`model` and `ref_model` cannot be the same object. If you want `ref_model` to be the " "same as `model`, you must mass a copy of it, or `None` if you use peft." ) # Model initialization if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model = create_model_from_path(model, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the KTOConfig, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) # Reference model initialization if isinstance(ref_model, str): ref_model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: ref_model_init_kwargs["device_map"] = None ref_model = create_model_from_path(ref_model, **ref_model_init_kwargs) # Initialize this variable to False. This helps tracking the case when `peft_module_casting_to_bf16` # has been called in order to properly call autocast if needed. self._peft_has_been_casted_to_bf16 = False if not is_peft_available() and peft_config is not None: raise ValueError( "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it with `pip install peft` to use the PEFT models" ) elif is_peft_available() and peft_config is not None: if isinstance(model, PeftModel): raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first " "merge and unload the existing adapter, save the resulting base model, and then pass that base " "model along with the new `peft_config` to the trainer." ) if getattr(model, "is_loaded_in_8bit", False) or getattr(model, "is_loaded_in_4bit", False): _support_gc_kwargs = hasattr( args, "gradient_checkpointing_kwargs" ) and "gradient_checkpointing_kwargs" in list( inspect.signature(prepare_model_for_kbit_training).parameters ) prepare_model_kwargs = {"use_gradient_checkpointing": args.gradient_checkpointing} if _support_gc_kwargs: prepare_model_kwargs["gradient_checkpointing_kwargs"] = args.gradient_checkpointing_kwargs model = prepare_model_for_kbit_training(model, **prepare_model_kwargs) elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) # get peft model with the given config model = get_peft_model(model, peft_config) if args.bf16 and getattr(model, "is_loaded_in_4bit", False): peft_module_casting_to_bf16(model) # If args.bf16 we need to explicitly call `generate` with torch amp autocast context manager self._peft_has_been_casted_to_bf16 = True # For models that use gradient_checkpointing, we need to attach a hook that enables input # to explicitly have `requires_grad=True`, otherwise training will either silently # fail or completely fail. elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) if args.generate_during_eval and not (is_wandb_available() or is_comet_available()): raise ValueError( "`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve." ) # KTO only supports causal language models, not encoder-decoder models if model is not None and hasattr(model.config, "is_encoder_decoder") and model.config.is_encoder_decoder: raise ValueError( "KTO only supports causal language models. Encoder-decoder models are not supported. " "Please use a causal LM (e.g., GPT, Llama, Mistral) instead of an encoder-decoder model (e.g., T5, BART)." ) self.is_peft_model = is_peft_available() and isinstance(model, PeftModel) self.model_adapter_name = model_adapter_name self.ref_adapter_name = ref_adapter_name if ref_model: self.ref_model = ref_model elif self.is_peft_model or args.precompute_ref_log_probs: # The `model` with adapters turned off will be used as the reference model self.ref_model = None else: self.ref_model = create_reference_model(model) if processing_class is None: raise ValueError( "max_length or a processing_class must be specified when using the default DPODataCollatorWithPadding" ) if args.max_length is None: logger.warning( "When using DPODataCollatorWithPadding, you should set `max_length` in the KTOTrainer's init" " it will be set to `512` by default, but you should do it yourself in the future.", ) max_length = 512 if args.max_length is not None: max_length = args.max_length if data_collator is None: data_collator = DPODataCollatorWithPadding( pad_token_id=processing_class.pad_token_id, ) if args.remove_unused_columns: args.remove_unused_columns = False # warn users logger.warning( "When using DPODataCollatorWithPadding, you should set `remove_unused_columns=False` in your KTOConfig" " we have set it for you, but you should do it yourself in the future.", ) self.use_dpo_data_collator = True else: self.use_dpo_data_collator = False # Disable dropout in the model and reference model if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) self.loss_type = args.loss_type self.max_length = max_length self.generate_during_eval = args.generate_during_eval self.processing_class = processing_class self.precompute_ref_log_probs = args.precompute_ref_log_probs # Not all losses require a KL calculation self.calculate_KL = True if self.loss_type in ["apo_zero_unpaired"]: self.calculate_KL = False # Since ref_logs are precomputed on the first call to get_train/eval_dataloader # keep track of first called to avoid computation of future calls self._precomputed_train_ref_log_probs = False self._precomputed_eval_ref_log_probs = False # metric self._stored_metrics = defaultdict(lambda: defaultdict(list)) # KTO parameter self.beta = args.beta self.desirable_weight = args.desirable_weight self.undesirable_weight = args.undesirable_weight self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) self.aux_loss_coef = getattr(model.config, "router_aux_loss_coef", 0.0) if self.aux_loss_enabled and self.aux_loss_coef == 0.0: logger.warning( "You set `output_router_logits` to `True` in the model config, but `router_aux_loss_coef` is set to " "`0.0`, meaning the auxiliary loss will not be used. Either set `router_aux_loss_coef` to a value " "greater than `0.0`, or set `output_router_logits` to `False` if you don't want to use the auxiliary " "loss.", ) # Compute that only on the main process for faster data processing. # see: https://github.com/huggingface/trl/pull/1255 with PartialState().main_process_first(): # Extract the prompt if needed train_dataset = train_dataset.map( maybe_extract_prompt, num_proc=args.dataset_num_proc, desc="Extracting prompt from train dataset" ) # Unpair the dataset if needed train_dataset = maybe_unpair_preference_dataset( train_dataset, args.dataset_num_proc, desc="Unpairing train dataset" ) # Apply the chat template if needed train_dataset = train_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc, desc="Applying chat template to train dataset", ) if eval_dataset is not None: eval_dataset = eval_dataset.map( maybe_extract_prompt, num_proc=args.dataset_num_proc, desc="Extracting prompt from eval dataset" ) eval_dataset = maybe_unpair_preference_dataset( eval_dataset, args.dataset_num_proc, desc="Unpairing eval dataset" ) eval_dataset = eval_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc, desc="Applying chat template to eval dataset", ) # Tokenize and prepare the training datasets train_dataset = train_dataset.map( _tokenize, batched=True, fn_kwargs={"tokenizer": self.processing_class}, num_proc=args.dataset_num_proc, desc="Tokenizing train dataset", ) fn_kwargs = { "prefix": "", "tokenizer": self.processing_class, "max_length": self.max_length, } train_dataset = train_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, desc="Processing tokenized train dataset", ) # Tokenize and prepare the eval datasets if eval_dataset is not None: eval_dataset = eval_dataset.map( _tokenize, fn_kwargs={"tokenizer": self.processing_class}, batched=True, num_proc=args.dataset_num_proc, desc="Tokenizing eval dataset", ) eval_dataset = eval_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, desc="Processing tokenized eval dataset", ) # Get KL datasets if needed if self.calculate_KL: if args.per_device_train_batch_size <= 1: raise ValueError( "Actual (not effective) batch size must be > 1. KTO will not work properly because the KL term will be equivalent to the implied reward." ) # create pairs for estimating the KL term by flipping the matched pairs in each batch of size total_batch_size # i.e., (x_1, y_1), ..., (x_n, y_n) --> (x_1, y_n), ..., (x_n, y_1) = (x'_1, y'_1), ..., (x'_n, y'_n) train_kl_dataset = train_dataset.map( _get_kl_dataset, batched=True, batch_size=args.per_device_train_batch_size, num_proc=args.dataset_num_proc, desc="Extracting KL train dataset", ) fn_kwargs["prefix"] = "KL_" train_kl_dataset = train_kl_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, remove_columns=[c for c in train_kl_dataset.column_names if c in train_dataset.column_names], desc="Processing tokenized train KL dataset", ) # merge the datasets train_dataset = concatenate_datasets([train_dataset, train_kl_dataset], axis=1) if eval_dataset is not None: # Get KL dataset eval_kl_dataset = eval_dataset.map( _get_kl_dataset, batched=True, batch_size=args.per_device_train_batch_size, num_proc=args.dataset_num_proc, desc="Extracting eval KL dataset", ) eval_kl_dataset = eval_kl_dataset.map( _process_tokens, fn_kwargs=fn_kwargs, num_proc=args.dataset_num_proc, remove_columns=[c for c in eval_kl_dataset.column_names if c in eval_dataset.column_names], desc="Processing tokenized eval KL dataset", ) # merge the datasets eval_dataset = concatenate_datasets([eval_dataset, eval_kl_dataset], axis=1) # calculate dataset desirability balance num_desirable = max(sum(train_dataset["label"]), 1) num_undesirable = max(len(train_dataset["label"]) - num_desirable, 1) # "label" is binary if num_desirable != num_undesirable: # The lower and upper bounds come from Eq. (8) of https://huggingface.co/papers/2402.01306 des_weight_lower_bound = round((num_undesirable * self.undesirable_weight / num_desirable) * 1, 2) des_weight_upper_bound = round((num_undesirable * self.undesirable_weight / num_desirable) * 1.33, 2) und_weight_lower_bound = round((num_desirable * self.desirable_weight / num_undesirable) / 1.33, 2) und_weight_upper_bound = round((num_desirable * self.desirable_weight / num_undesirable) / 1, 2) des_weight_in_range = des_weight_lower_bound <= self.desirable_weight <= des_weight_upper_bound und_weight_in_range = und_weight_lower_bound <= self.undesirable_weight <= und_weight_upper_bound if not (des_weight_in_range or und_weight_in_range): logger.warning( "You have different amounts of desirable/positive and undesirable/negative examples but the " "weights on the desirable and undesirable losses don't seem to be in an ideal range. Based " f"on your data, we recommend EITHER " f"desirable_weight in [{des_weight_lower_bound}, {des_weight_upper_bound}] or " f"undesirable_weight in [{und_weight_lower_bound}, {und_weight_upper_bound}] (but NOT BOTH). " "See the documentation on how to optimally set these weights.", ) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, model_init=model_init, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) if not hasattr(self, "accelerator"): raise AttributeError( "Your `Trainer` does not have an `accelerator` object. Consider upgrading `transformers`." ) # Deepspeed Zero-3 does not support precompute_ref_log_probs if self.is_deepspeed_enabled: if self.accelerator.state.deepspeed_plugin.zero_stage == 3 and self.precompute_ref_log_probs: raise ValueError( "You cannot use `precompute_ref_log_probs=True` with Deepspeed ZeRO-3. Please set `precompute_ref_log_probs=False`." ) if self.ref_model is None: if not (self.is_peft_model or self.precompute_ref_log_probs): raise ValueError( "No reference model and model is not a Peft model. Try setting `precompute_ref_log_probs=True`" ) else: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) # Import Liger kernel if enabled if self.args.use_liger_kernel: if not is_liger_kernel_available(): raise ImportError( "You set `use_liger_kernel=True` but the liger kernel is not available. " "Please install liger-kernel first: `pip install liger-kernel`" ) if self.loss_type in ["apo_zero_unpaired"]: raise ValueError( "You cannot set `loss_type='apo_zero_unpaired'` with liger-kernel." "Only KTO loss is supported with liger-kernel." ) if self.precompute_ref_log_probs: raise ValueError( "You cannot use `precompute_ref_log_probs=True` with liger kernel. Please set " "`precompute_ref_log_probs=False`." ) if self.is_peft_model or self.ref_adapter_name is not None: raise ValueError( "You cannot use `use_liger_kernel=True` with Peft models. Please set `use_liger_kernel=False`." ) self.kto_loss_fn = LigerFusedLinearKTOLoss(beta=self.beta, use_ref_model=(self.ref_model is not None)) @contextmanager def null_ref_context(self): """Context manager for handling null reference model (that is, peft adapter manipulation).""" with ( self.accelerator.unwrap_model(self.model).disable_adapter() if self.is_peft_model and not self.ref_adapter_name else nullcontext() ): if self.ref_adapter_name: self.model.set_adapter(self.ref_adapter_name) yield if self.ref_adapter_name: self.model.set_adapter(self.model_adapter_name or "default") def get_train_dataloader(self) -> DataLoader: """ Returns the training [`~torch.utils.data.DataLoader`]. Subclass of transformers.src.transformers.trainer.get_train_dataloader to precompute `ref_log_probs`. """ if self.precompute_ref_log_probs and not self._precomputed_train_ref_log_probs: dataloader_params = { "batch_size": self.args.per_device_train_batch_size, "collate_fn": self.data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "shuffle": False, } # prepare dataloader data_loader = self.accelerator.prepare(DataLoader(self.train_dataset, **dataloader_params)) reference_completion_logps = [] reference_KL_logps = [] for padded_batch in tqdm(iterable=data_loader, desc="Train dataset reference log probs"): reference_completion_logp, reference_KL_logp = self.compute_reference_log_probs(padded_batch) reference_completion_logp = self.accelerator.gather_for_metrics(reference_completion_logp) reference_completion_logps.append(reference_completion_logp.cpu()) if self.calculate_KL: reference_KL_logp = self.accelerator.gather_for_metrics(reference_KL_logp) reference_KL_logps.append(reference_KL_logp.cpu()) self.train_dataset = self.train_dataset.add_column( name="reference_logps", column=torch.cat(reference_completion_logps).float().numpy() ) if self.calculate_KL: self.train_dataset = self.train_dataset.add_column( name="reference_KL_logps", column=torch.cat(reference_KL_logps).float().numpy() ) self._precomputed_train_ref_log_probs = True return super().get_train_dataloader() def get_eval_dataloader(self, eval_dataset: Dataset | None = None) -> DataLoader: """ Returns the evaluation [`~torch.utils.data.DataLoader`]. Subclass of transformers.src.transformers.trainer.get_eval_dataloader to precompute `ref_log_probs`. Args: eval_dataset (`torch.utils.data.Dataset`, *optional*): If provided, will override `self.eval_dataset`. If it is a [`~datasets.Dataset`], columns not accepted by the `model.forward()` method are automatically removed. It must implement `__len__`. """ if eval_dataset is None and self.eval_dataset is None: raise ValueError("Trainer: evaluation requires an eval_dataset.") eval_dataset = eval_dataset if eval_dataset is not None else self.eval_dataset if self.precompute_ref_log_probs and not self._precomputed_eval_ref_log_probs: dataloader_params = { "batch_size": self.args.per_device_eval_batch_size, "collate_fn": self.data_collator, "num_workers": self.args.dataloader_num_workers, "pin_memory": self.args.dataloader_pin_memory, "shuffle": False, } # prepare dataloader data_loader = self.accelerator.prepare(DataLoader(eval_dataset, **dataloader_params)) reference_completion_logps = [] reference_KL_logps = [] for padded_batch in tqdm(iterable=data_loader, desc="Eval dataset reference log probs"): reference_completion_logp, reference_KL_logp = self.compute_reference_log_probs(padded_batch) reference_completion_logp = self.accelerator.gather_for_metrics(reference_completion_logp) reference_completion_logps.append(reference_completion_logp.cpu()) if self.calculate_KL: reference_KL_logp = self.accelerator.gather_for_metrics(reference_KL_logp) reference_KL_logps.append(reference_KL_logp.cpu()) eval_dataset = eval_dataset.add_column( name="reference_logps", column=torch.cat(reference_completion_logps).float().numpy() ) if self.calculate_KL: eval_dataset = eval_dataset.add_column( name="reference_KL_logps", column=torch.cat(reference_KL_logps).float().numpy() ) # Save calculated reference_chosen_logps and reference_rejected_logps to the eval_dataset for subsequent runs if self.eval_dataset is not None: self.eval_dataset = eval_dataset self._precomputed_eval_ref_log_probs = True return super().get_eval_dataloader(eval_dataset=eval_dataset) def compute_reference_log_probs(self, padded_batch: dict) -> dict: """Computes log probabilities of the reference model for a single padded batch of a KTO specific dataset.""" with torch.no_grad(): if self.ref_model is None: with self.null_ref_context(): completion_logits = self.model( padded_batch["completion_input_ids"], attention_mask=padded_batch["completion_attention_mask"], ).logits if self.calculate_KL: KL_logits = self.model( padded_batch["KL_completion_input_ids"], attention_mask=padded_batch["KL_completion_attention_mask"], ).logits else: completion_logits = self.ref_model( padded_batch["completion_input_ids"], attention_mask=padded_batch["completion_attention_mask"] ).logits if self.calculate_KL: KL_logits = self.ref_model( padded_batch["KL_completion_input_ids"], attention_mask=padded_batch["KL_completion_attention_mask"], ).logits completion_logps = self.get_batch_logps( completion_logits, padded_batch["completion_labels"], average_log_prob=False, ) if self.calculate_KL: KL_logps = self.get_batch_logps( KL_logits, padded_batch["KL_completion_labels"], average_log_prob=False, ) else: KL_logps = None return completion_logps, KL_logps @staticmethod def get_batch_logps( logits: torch.FloatTensor, labels: torch.LongTensor, average_log_prob: bool = False, ) -> torch.FloatTensor: """Compute the log probabilities of the given labels under the given logits. Args: logits: Logits of the model (unnormalized). Shape: (batch_size, sequence_length, vocab_size) labels: Labels for which to compute the log probabilities. Label tokens with a value of `-100` are ignored. Shape: (batch_size, sequence_length) average_log_prob: If True, return the average log probability per (non-masked) token. Otherwise, return the sum of the log probabilities of the (non-masked) tokens. Returns: A tensor of shape (batch_size,) containing the average/sum log probabilities of the given labels under the given logits. """ if logits.shape[:-1] != labels.shape: raise ValueError("Logits (batch and sequence length dim) and labels must have the same shape.") # For causal LM, shift labels and logits by one position labels = labels[:, 1:].clone() logits = logits[:, :-1, :] loss_mask = labels != -100 # dummy token; we'll ignore the losses on these tokens later labels[labels == -100] = 0 per_token_logps = selective_log_softmax(logits, labels) if average_log_prob: return (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) else: return (per_token_logps * loss_mask).sum(-1) def forward( self, model: nn.Module, batch: dict[str, list | torch.LongTensor] ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: KL_logps = self._compute_kl_logps(model, batch) model_kwargs = {} if self.aux_loss_enabled: model_kwargs["output_router_logits"] = True outputs = model( batch["completion_input_ids"], attention_mask=batch["completion_attention_mask"], **model_kwargs, ) completion_logits = outputs.logits completion_logps = self.get_batch_logps( completion_logits, batch["completion_labels"], average_log_prob=False, ) if completion_logps.shape[0] != len(batch["label"]): raise ValueError( "There is a mismatch between the number of examples in this batch and the number of " "examples for which an output sequence was predicted." ) # Use torch.nonzero for efficient tensor index selection device = completion_logits.device labels = torch.as_tensor(batch["label"], dtype=torch.bool, device=device) chosen_idx = torch.nonzero(labels, as_tuple=False).view(-1) rejected_idx = torch.nonzero(~labels, as_tuple=False).view(-1) # Use index_select for efficient CUDA operations chosen_logps = completion_logps.index_select(0, chosen_idx) rejected_logps = completion_logps.index_select(0, rejected_idx) chosen_logits = completion_logits.index_select(0, chosen_idx) rejected_logits = completion_logits.index_select(0, rejected_idx) if self.aux_loss_enabled: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, KL_logps, outputs.aux_loss) else: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, KL_logps) def kto_loss( self, policy_chosen_logps: torch.FloatTensor, policy_rejected_logps: torch.FloatTensor, policy_KL_logps: torch.FloatTensor, reference_chosen_logps: torch.FloatTensor, reference_rejected_logps: torch.FloatTensor, reference_KL_logps: torch.FloatTensor, ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Compute the KTO loss for a batch of policy and reference model log probabilities. Args: policy_chosen_logps: Log probabilities of the policy model for the chosen responses. Shape: (num(chosen) in batch_size,) policy_rejected_logps: Log probabilities of the policy model for the rejected responses. Shape: (num(rejected) in batch_size,) policy_KL_logps: Log probabilities of the policy model for the KL responses. Shape: (batch_size,) reference_chosen_logps: Log probabilities of the reference model for the chosen responses. Shape: (num(chosen) in batch_size,) reference_rejected_logps: Log probabilities of the reference model for the rejected responses. Shape: (num(rejected) in batch_size,) reference_KL_logps: Log probabilities of the reference model for the KL responses. Shape: (batch_size,) Returns: A tuple of four tensors: (losses, chosen_rewards, rejected_rewards, KL). The losses tensor contains the KTO loss for each example in the batch. The chosen_rewards and rejected_rewards tensors contain the rewards for the chosen and rejected responses, respectively. The KL tensor contains the detached KL divergence estimate between the policy and reference models. """ if self.calculate_KL: kl = (policy_KL_logps - reference_KL_logps).mean().detach() kl = self.accelerator.gather_for_metrics(kl).mean().clamp(min=0) else: kl = torch.zeros(1).to(policy_chosen_logps.device) # Chosen losses if policy_chosen_logps.shape[0] != 0 or reference_chosen_logps.shape[0] != 0: chosen_logratios = policy_chosen_logps - reference_chosen_logps if self.loss_type == "kto": # Eqn (7) of the KTO paper (https://huggingface.co/papers/2402.01306) chosen_losses = 1 - F.sigmoid(self.beta * (chosen_logratios - kl)) elif self.loss_type == "apo_zero_unpaired": # Unpaired variant of Eqn (7) of the APO paper (https://huggingface.co/papers/2408.06266) # Use this loss when you believe the chosen outputs are better than your model's default output chosen_losses = 1 - F.sigmoid(self.beta * chosen_logratios) chosen_rewards = self.beta * chosen_logratios.detach() else: # lists can't be empty -- if they are, then accelerate.gather will hang chosen_losses = torch.Tensor([]).to(self.accelerator.device) chosen_rewards = torch.Tensor([]).to(self.accelerator.device) # Rejected losses if policy_rejected_logps.shape[0] != 0 or reference_rejected_logps.shape[0] != 0: rejected_logratios = policy_rejected_logps - reference_rejected_logps if self.loss_type == "kto": rejected_losses = 1 - F.sigmoid(self.beta * (kl - rejected_logratios)) elif self.loss_type == "apo_zero_unpaired": rejected_losses = F.sigmoid(self.beta * rejected_logratios) rejected_rewards = self.beta * rejected_logratios.detach() else: # lists can't be empty -- if they are, then accelerate.gather will hang rejected_losses = torch.Tensor([]).to(self.accelerator.device) rejected_rewards = torch.Tensor([]).to(self.accelerator.device) losses = torch.cat( (self.desirable_weight * chosen_losses, self.undesirable_weight * rejected_losses), 0, ) return losses, chosen_rewards, rejected_rewards, kl def _compute_kl_logps(self, model, batch): """Compute KL log probabilities for a given batch.""" KL_logps = None if self.calculate_KL: KL_model_kwargs = { "input_ids": batch["KL_completion_input_ids"], "attention_mask": batch["KL_completion_attention_mask"], } with torch.no_grad(): KL_logits = model(**KL_model_kwargs).logits KL_logps = self.get_batch_logps( KL_logits, batch["KL_completion_labels"], average_log_prob=False, ) return KL_logps def _compute_loss_liger(self, model, batch): """ Compute the KTO loss using the Liger-Kernel's LigerFusedLinearKTOLoss. Args: model: The policy model used for generating log probabilities and outputs. It could be an encoder-decoder model or a regular language model. batch: A dictionary containing the input data and labels for the batch. Returns: A dictionary containing the following keys: - "loss": The computed KTO loss for the batch. - "chosen_logits_sum": Sum of the logits for the chosen responses from the policy model. - "rejected_logits_sum": Sum of the logits for the rejected responses from the policy model. - "chosen_logps": Log probabilities of the chosen responses from the policy model. - "rejected_logps": Log probabilities of the rejected responses from the policy model. - "chosen_rewards": Rewards for the chosen responses. - "rejected_rewards": Rewards for the rejected responses. - "kl": The KL divergence between the policy and reference models (detached). If auxiliary loss is enabled, the dictionary will also include: - "aux_loss": The auxiliary loss from the model outputs. """ policy_KL_logps = self._compute_kl_logps(model, batch) reference_KL_logps = self._compute_kl_logps(self.ref_model, batch) if self.calculate_KL: kl = (policy_KL_logps - reference_KL_logps).mean().detach() kl = self.accelerator.gather_for_metrics(kl).mean().clamp(min=0) else: kl = torch.zeros(1).to(self.accelerator.device) model_kwargs = {} if self.aux_loss_enabled: model_kwargs["output_router_logits"] = True # skip the lm head and get the last hidden state base_model = model.get_decoder() outputs = base_model( batch["completion_input_ids"], attention_mask=batch["completion_attention_mask"], use_cache=False, **model_kwargs, ) # reference model ref_base_model = self.ref_model.get_decoder() ref_outputs = ref_base_model( batch["completion_input_ids"], attention_mask=batch["completion_attention_mask"], use_cache=False, **model_kwargs, ) lm_head = model.get_output_embeddings() ref_lm_head = self.ref_model.get_output_embeddings() ( loss, ( chosen_logps_sum, rejected_logps_sum, chosen_logits_sum, rejected_logits_sum, chosen_rewards_sum, rejected_rewards_sum, ), ) = self.kto_loss_fn( _input=outputs.last_hidden_state[:, :-1], lin_weight=lm_head.weight, target=batch["completion_labels"][:, 1:], bias=lm_head.bias if hasattr(lm_head, "bias") else None, preference_labels=torch.tensor(batch["label"], dtype=torch.bool).to(self.accelerator.device), ref_input=ref_outputs.last_hidden_state[:, :-1], ref_weight=ref_lm_head.weight, ref_bias=ref_lm_head.bias if hasattr(lm_head, "bias") else None, kl=kl, ) output = { "loss": loss, "chosen_logits_sum": chosen_logits_sum, "rejected_logits_sum": rejected_logits_sum, "chosen_logps_sum": chosen_logps_sum, "rejected_logps_sum": rejected_logps_sum, "chosen_rewards_sum": chosen_rewards_sum, "rejected_rewards_sum": rejected_rewards_sum, "kl": kl, } if self.aux_loss_enabled: output["aux_loss"] = outputs.aux_loss return output def get_batch_loss_metrics( self, model, batch: dict[str, list | torch.LongTensor], ): """Compute the KTO loss and other metrics for the given batch of inputs for train or test.""" metrics = {} batch = {k: (v.to(self.accelerator.device) if isinstance(v, torch.Tensor) else v) for k, v in batch.items()} labels = torch.tensor(batch["label"]) num_chosen = labels.sum().to(self.accelerator.device) num_rejected = (len(labels) - num_chosen).to(self.accelerator.device) if self.args.use_liger_kernel: model_output = self._compute_loss_liger(model, batch) losses = model_output["loss"] policy_chosen_logits = model_output["chosen_logits_sum"] policy_rejected_logits = model_output["rejected_logits_sum"] policy_chosen_logps = model_output["chosen_logps_sum"] policy_rejected_logps = model_output["rejected_logps_sum"] chosen_rewards = model_output["chosen_rewards_sum"] rejected_rewards = model_output["rejected_rewards_sum"] kl = model_output["kl"] if self.aux_loss_enabled: aux_loss = model_output["aux_loss"] else: forward_output = self.forward(model, batch) ( policy_chosen_logps, policy_rejected_logps, policy_chosen_logits, policy_rejected_logits, policy_KL_logps, ) = forward_output[:5] if self.aux_loss_enabled: aux_loss = forward_output[5] # if reference_logps in batch use them, otherwise use the reference model if "reference_logps" in batch: # Convert Python lists to tensor indices for efficient CUDA operations device = batch["reference_logps"].device labels = torch.as_tensor(batch["label"], dtype=torch.bool, device=device) chosen_idx = torch.nonzero(labels, as_tuple=False).view(-1) rejected_idx = torch.nonzero(~labels, as_tuple=False).view(-1) # Use index_select for efficient CUDA operations reference_chosen_logps = batch["reference_logps"].index_select(0, chosen_idx) reference_rejected_logps = batch["reference_logps"].index_select(0, rejected_idx) if self.calculate_KL: reference_KL_logps = batch["reference_KL_logps"] else: reference_KL_logps = None else: with torch.no_grad(): if self.ref_model is None: with self.null_ref_context(): ( reference_chosen_logps, reference_rejected_logps, _, _, reference_KL_logps, ) = self.forward(self.model, batch)[:5] else: ( reference_chosen_logps, reference_rejected_logps, _, _, reference_KL_logps, ) = self.forward(self.ref_model, batch)[:5] losses, chosen_rewards, rejected_rewards, kl = self.kto_loss( policy_chosen_logps, policy_rejected_logps, policy_KL_logps, reference_chosen_logps, reference_rejected_logps, reference_KL_logps, ) metrics["kl"] = kl.item() all_num_chosen = self.accelerator.gather_for_metrics(num_chosen).sum().item() all_num_rejected = self.accelerator.gather_for_metrics(num_rejected).sum().item() if all_num_chosen > 0: metrics["rewards/chosen_sum"] = ( self.accelerator.gather_for_metrics(chosen_rewards.nansum()).nansum().item() ) metrics["logps/chosen_sum"] = ( self.accelerator.gather_for_metrics(policy_chosen_logps.nansum()).nansum().item() ) metrics["logits/chosen_sum"] = ( self.accelerator.gather_for_metrics(policy_chosen_logits.nansum()).nansum().item() ) metrics["count/chosen"] = all_num_chosen if all_num_rejected > 0: metrics["rewards/rejected_sum"] = ( self.accelerator.gather_for_metrics(rejected_rewards.nansum()).nansum().item() ) metrics["logps/rejected_sum"] = ( self.accelerator.gather_for_metrics(policy_rejected_logps.nansum()).nansum().item() ) metrics["logits/rejected_sum"] = ( self.accelerator.gather_for_metrics(policy_rejected_logits.nansum()).nansum().item() ) metrics["count/rejected"] = all_num_rejected loss = losses.nanmean() if self.aux_loss_enabled: loss += self.aux_loss_coef * aux_loss return loss, metrics def compute_loss( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], return_outputs=False, num_items_in_batch=None, ) -> torch.Tensor | tuple[torch.Tensor, dict[str, torch.Tensor]]: compute_loss_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with compute_loss_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs) # Make sure to move the loss to the device the original accumulating loss is at back in the `Trainer` class: loss = loss.to(self.args.device) # force log the metrics if self.accelerator.is_main_process: self.store_metrics(metrics, train_eval="train") if return_outputs: return (loss, metrics) return loss def store_metrics(self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train") -> None: for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) def _get_train_sampler(self, dataset: Dataset | None = None) -> torch.utils.data.Sampler | None: if dataset is None: dataset = self.train_dataset if dataset is None or not has_length(dataset): return None return SequentialSampler(dataset) def generate_from_model_and_ref(self, model, batch: dict[str, torch.LongTensor]) -> tuple[str, str]: """Generate samples from the model and reference model for the given batch of inputs.""" # If one uses `generate_during_eval` with peft + bf16, we need to explicitly call generate with # the torch amp context manager as some hidden states are silently casted to full precision. generate_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with generate_context_manager: policy_output = model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) # if reference_output in batch use that otherwise use the reference model if "reference_output" in batch: reference_output = batch["reference_output"] else: if self.ref_model is None: with self.null_ref_context(): reference_output = self.model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) else: reference_output = self.ref_model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) policy_output = pad_to_length(policy_output, self.max_length, self.processing_class.pad_token_id) policy_output_decoded = self.processing_class.batch_decode(policy_output, skip_special_tokens=True) reference_output = pad_to_length(reference_output, self.max_length, self.processing_class.pad_token_id) reference_output_decoded = self.processing_class.batch_decode(reference_output, skip_special_tokens=True) return policy_output_decoded, reference_output_decoded def prediction_step( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], prediction_loss_only: bool, ignore_keys: list[str] | None = None, ): if ignore_keys is None: if hasattr(model, "config"): ignore_keys = getattr(model.config, "keys_to_ignore_at_inference", []) else: ignore_keys = [] prediction_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with torch.no_grad(), prediction_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs) # force log the metrics if self.accelerator.is_main_process: self.store_metrics(metrics, train_eval="eval") if prediction_loss_only: return (loss.detach(), None, None) # logits for the chosen and rejected samples from model logits_dict = {} if "logits/chosen_sum" in metrics: logits_dict["eval_logits/chosen"] = metrics["logits/chosen_sum"] if "logits/rejected_sum" in metrics: logits_dict["eval_logits/rejected"] = metrics["logits/rejected_sum"] logits = [v for k, v in logits_dict.items() if k not in ignore_keys] logits = torch.tensor(logits, device=self.accelerator.device) labels = torch.zeros(logits.shape[0], device=self.accelerator.device) return (loss.detach(), logits, labels) def evaluation_loop( self, dataloader: DataLoader, description: str, prediction_loss_only: bool | None = None, ignore_keys: list[str] | None = None, metric_key_prefix: str = "eval", ) -> EvalLoopOutput: """ Overriding built-in evaluation loop to store metrics for each batch. Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. Works both with or without labels. """ # Sample and save to game log if requested (for one batch to save time) if self.generate_during_eval: # Generate random indices within the range of the total number of samples num_samples = len(dataloader.dataset) random_indices = random.sample(range(num_samples), k=self.args.eval_batch_size) # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader random_batch_dataset = dataloader.dataset.select(random_indices) random_batch = self.data_collator(random_batch_dataset) random_batch = self._prepare_inputs(random_batch) target_labels = torch.tensor(random_batch["label"], dtype=torch.bool, device=self.accelerator.device) target_indices = torch.where(~target_labels)[0] target_batch = { "prompt_input_ids": random_batch["prompt_input_ids"][target_indices], "prompt_attention_mask": random_batch["prompt_attention_mask"][target_indices], "prompt": itemgetter(*target_indices)(random_batch["prompt"]), } policy_output_decoded, ref_output_decoded = self.generate_from_model_and_ref(self.model, target_batch) table = pd.DataFrame( columns=["Prompt", "Policy", "Ref Model"], data=[ [prompt, pol[len(prompt) :], ref[len(prompt) :]] for prompt, pol, ref in zip( target_batch["prompt"], policy_output_decoded, ref_output_decoded, strict=True ) ], ) if "wandb" in self.args.report_to: wandb.log({"game_log": wandb.Table(data=table)}) if "comet_ml" in self.args.report_to: log_table_to_comet_experiment( name="game_log.csv", table=table, ) # Base evaluation initial_output = super().evaluation_loop( dataloader, description, prediction_loss_only, ignore_keys, metric_key_prefix ) return initial_output def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: logs (`dict[str, float]`): The values to log. start_time (`float`, *optional*): Start time of the training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" # train metrics should have no prefix, eval should have 'eval_' prefix = "eval_" if train_eval == "eval" else "" # accumulate average metrics from sums and lengths for split in ["chosen", "rejected"]: if f"count/{split}" in self._stored_metrics[train_eval]: count_sum = torch.Tensor(self._stored_metrics[train_eval][f"count/{split}"]).sum().item() for metric in ["rewards", "logps", "logits"]: logs[f"{prefix}{metric}/{split}"] = ( torch.Tensor(self._stored_metrics[train_eval][f"{metric}/{split}_sum"]).sum().item() / count_sum ) # delete obsolete metric del self._stored_metrics[train_eval][f"{metric}/{split}_sum"] del self._stored_metrics[train_eval][f"count/{split}"] # calculate reward margin if f"{prefix}rewards/chosen" in logs and f"{prefix}rewards/rejected" in logs: logs[f"{prefix}rewards/margins"] = logs[f"{prefix}rewards/chosen"] - logs[f"{prefix}rewards/rejected"] # Add averaged stored metrics to logs for key, metrics in self._stored_metrics[train_eval].items(): logs[f"{prefix}{key}"] = torch.Tensor(metrics).mean().item() del self._stored_metrics[train_eval] return super().log(logs, start_time) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/merge_model_callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import torch from huggingface_hub import HfApi from transformers import TrainerCallback from ..import_utils import is_mergekit_available from ..trainer.utils import get_config_model_id if is_mergekit_available(): from mergekit.config import MergeConfiguration from mergekit.merge import MergeOptions, run_merge # Logger for module-level logging logger = logging.getLogger(__name__) def upload_model_to_hf(folder_path: str, repo_id: str): api = HfApi() # Create the repository if it doesn't exist repo = api.create_repo(repo_id, repo_type="model") # Upload the folder to the specified repository api.upload_folder( folder_path=folder_path, repo_id=repo.repo_id, repo_type=repo.repo_type, ) class MergeConfig: r""" Configuration class for merging two models using `mergekit`. This class provides a structured way to configure and generate merge configurations for various merge methods, such as `linear`, `ties`, `dare_ties`, and `slerp`. Args: method (`str`, *optional*, defaults to `"linear"`): Merge method to use. Supported methods include: - `"linear"`: Linearly combines two models with specified weights. - `"ties"`: Combines two models using the TIES method with density parameters. - `"dare_ties"`: A variant of TIES for domain adaptation. - `"slerp"`: Combines models using spherical linear interpolation. Note: For more details about the merge methods and how they are implemented, see the [MergeKit GitHub repository](https://github.com/arcee-ai/mergekit?tab=readme-ov-file#merge-methods). Attributes: method (`str`): The merge method to use. policy_model_path (`str` or `None`): Path to the policy model. target_model_path (`str` or `None`): Path to the target model. policy_model_weight (`float`): Weight for the policy model (for `linear` and `ties` methods). target_model_weight (`float`): Weight for the target model (for `linear` and `ties` methods). policy_model_density (`list[float]`): Density parameters for the policy model (for `ties` and `dare_ties`). target_model_density (`list[float]`): Density parameters for the target model (for `ties` and `dare_ties`). normalize (`float` or `None`): Normalization factor for the TIES method. t_values (`float` or `None`): Interpolation factor for the SLERP method. dtype (`str`): Data type to use for merging, e.g., `"float16"`. """ def __init__(self, method: str = "linear"): if not is_mergekit_available(): raise ImportError("MergeConfig requires the `mergekit` extra. To install, run `pip install mergekit`.") self.method = method self.policy_model_path = None self.target_model_path = None # Initialize relevant parameters based on the method if method == "linear": self.policy_model_weight = 0.5 self.target_model_weight = 0.5 self.dtype = "float16" elif method == "ties": self.policy_model_weight = 1.0 self.policy_model_density = [1.0, 0.7, 0.1] self.target_model_weight = 1.0 self.target_model_density = [1.0] self.normalize = 1.0 self.dtype = "float16" elif method == "dare_ties": self.policy_model_weight = 1.0 self.policy_model_density = [1.0, 0.7, 0.1] self.target_model_weight = 1.0 self.target_model_density = [1.0] self.normalize = 1.0 self.dtype = "float16" elif method == "slerp": self.t_values = 0.5 self.dtype = "float16" else: raise ValueError(f"Unsupported merge method: {method}") def create_merge_config_linear(self) -> "MergeConfiguration": """ Creates a merge configuration for a linear merge of two models with specified weights. """ # Create the merge configuration dictionary merge_config_dict = { "dtype": self.dtype, "merge_method": "linear", "models": [ {"model": self.policy_model_path, "parameters": {"weight": self.policy_model_weight}}, {"model": self.target_model_path, "parameters": {"weight": self.target_model_weight}}, ], } # Create the MergeConfiguration from the dictionary merge_config = MergeConfiguration.model_validate(merge_config_dict) return merge_config def create_merge_config_ties(self) -> "MergeConfiguration": """ Creates a merge configuration for a TIES merge of two models, with specified weights and densities. """ # Create the TIES merge configuration dictionary merge_config_dict = { "merge_method": "ties", "slices": None, # Optional slices if needed "models": [ { "model": { "model": {"path": self.target_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "parameters": {"density": self.target_model_density, "weight": self.target_model_weight}, }, { "model": { "model": {"path": self.policy_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "parameters": {"density": self.policy_model_density, "weight": self.policy_model_weight}, }, ], "parameters": {"normalize": self.normalize}, "base_model": { "model": {"path": self.policy_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "dtype": self.dtype, "tokenizer_source": None, "tokenizer": None, "chat_template": None, "out_dtype": None, } # Create the MergeConfiguration from the dictionary merge_config = MergeConfiguration.model_validate(merge_config_dict) return merge_config def create_merge_config_dare_ties(self) -> "MergeConfiguration": """ Creates a merge configuration for a DARE TIES merge of two models, with specified weights and densities. """ # Create the DARE TIES merge configuration dictionary merge_config_dict = { "merge_method": "dare_ties", "slices": None, # Optional slices if needed "models": [ { "model": { "model": {"path": self.target_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "parameters": {"density": self.target_model_density, "weight": self.target_model_weight}, }, { "model": { "model": {"path": self.policy_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "parameters": {"density": self.policy_model_density, "weight": self.policy_model_weight}, }, ], "parameters": {"normalize": self.normalize}, "base_model": { "model": {"path": self.policy_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "dtype": self.dtype, "tokenizer_source": None, "tokenizer": None, "chat_template": None, "out_dtype": None, } # Create the MergeConfiguration from the dictionary merge_config = MergeConfiguration.model_validate(merge_config_dict) return merge_config def create_merge_config_slerp(self) -> "MergeConfiguration": """ Creates a merge configuration for a SLERP merge of a model with a base model. """ # Create the SLERP merge configuration dictionary merge_config_dict = { "merge_method": "slerp", "slices": None, # Optional slices if needed "models": [ { "model": { "model": {"path": self.target_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "parameters": None, # No specific parameters for SLERP model } ], "parameters": { "t": self.t_values # Set the t values for SLERP }, "base_model": { "model": {"path": self.policy_model_path, "revision": None}, "lora": None, "override_architecture": None, }, "dtype": self.dtype, "tokenizer_source": None, "tokenizer": None, "chat_template": None, "out_dtype": None, } # Create the MergeConfiguration from the dictionary merge_config = MergeConfiguration.model_validate(merge_config_dict) return merge_config def create(self) -> "MergeConfiguration": if self.method == "linear": return self.create_merge_config_linear() elif self.method == "ties": return self.create_merge_config_ties() elif self.method == "dare_ties": return self.create_merge_config_dare_ties() elif self.method == "slerp": return self.create_merge_config_slerp() def merge_models(config: "MergeConfiguration", out_path: str): """ Merge two models using mergekit Args: config (`MergeConfiguration`): The merge configuration. out_path (`str`): The output path for the merged model. """ if not is_mergekit_available(): raise ImportError("merge_models requires the `mergekit` extra. To install, run `pip install mergekit`.") run_merge( config, out_path=out_path, options=MergeOptions( device="auto", cuda=torch.cuda.is_available(), copy_tokenizer=True, lazy_unpickle=False, low_cpu_memory=False, ), ) class MergeModelCallback(TrainerCallback): r""" A [`~transformers.TrainerCallback`] that merges the policy model (the model being trained) with another model based on a merge configuration. Args: merge_config ([`experimental.merge_model_callback.MergeConfig`], *optional*): Configuration used for the merging process. If not provided, the default [`~experimental.merge_model_callback.MergeConfig`] is used. merge_at_every_checkpoint (`bool`, *optional*, defaults to `False`): Whether to merge the model at every checkpoint. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the merged model to the Hub after merging. Example: ```python from trl.experimental.merge_model_callback import MergeConfig, MergeModelCallback config = MergeConfig() merge_callback = MergeModelCallback(config) trainer = DPOTrainer(..., callbacks=[merge_callback]) ``` """ def __init__( self, merge_config: "MergeConfig | None" = None, merge_at_every_checkpoint: bool = False, push_to_hub: bool = False, ): if not is_mergekit_available(): raise ImportError( "MergeModelCallback requires the `mergekit` extra. To install, run `pip install mergekit`." ) self.merge_config = merge_config or MergeConfig() self.merge_at_every_checkpoint = merge_at_every_checkpoint self.push_to_hub = push_to_hub def _merge_and_maybe_push(self, output_dir, global_step, model): checkpoint_path = os.path.join(output_dir, f"checkpoint-{global_step}") self.merge_config.policy_model_path = checkpoint_path if self.merge_config.target_model_path is None: self.merge_config.target_model_path = get_config_model_id(model.config) merge_path = os.path.join(checkpoint_path, "merged") merge_models(self.merge_config.create(), merge_path) if self.push_to_hub: repo_name = f"{output_dir}_checkpoint-{global_step}_merged" upload_model_to_hf(merge_path, repo_name) def on_save(self, args, state, control, model=None, **kwargs): if self.merge_at_every_checkpoint: self._merge_and_maybe_push(args.output_dir, state.global_step, model) def on_train_end(self, args, state, control, model=None, **kwargs): if not self.merge_at_every_checkpoint: self._merge_and_maybe_push(args.output_dir, state.global_step, model) ================================================ FILE: trl/experimental/minillm/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .minillm_config import MiniLLMConfig from .minillm_trainer import MiniLLMTrainer __all__ = ["MiniLLMConfig", "MiniLLMTrainer"] ================================================ FILE: trl/experimental/minillm/minillm_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from transformers import TrainingArguments from ...trainer.grpo_config import GRPOConfig @dataclass class MiniLLMConfig(GRPOConfig): """ Configuration class for [`MiniLLMTrainer`]. This class includes only the parameters that are specific to MiniLLM training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] and [`GRPOConfig`] documentation. Args: teacher_model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the teacher model from a string. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. rkl_advantage (`bool`, *optional*, defaults to `True`): Whether to add the reverse KL advantage to the reward advantage. single_step_decomposition (`bool`, *optional*, defaults to `True`): Whether to use single-step decomposition for the KL divergence computation. kd_temperature (`float`, *optional*, defaults to `1.0`): Temperature for knowledge distillation. Higher temperatures produce softer probability distributions over classes. gamma (`float`, *optional*, defaults to `0.0`): Discount factor for future rewards in reinforcement learning. length_normalization (`bool`, *optional*, defaults to `True`): Whether to apply length normalization to the rewards. """ _VALID_DICT_FIELDS = GRPOConfig._VALID_DICT_FIELDS + ["teacher_model_init_kwargs"] teacher_model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the " "teacher model from a string." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropouts in `model`."}, ) rkl_advantage: bool = field( default=True, metadata={"help": "Whether to add the reverse KL advantage to the reward advantage."}, ) single_step_decomposition: bool = field( default=True, metadata={"help": "Whether to use single-step decomposition for the KL divergence computation."}, ) kd_temperature: float = field( default=1.0, metadata={ "help": "Temperature for knowledge distillation. Higher temperatures produce softer probability " "distributions over classes." }, ) gamma: float = field( default=0.0, metadata={"help": "Discount factor for future rewards in reinforcement learning."}, ) length_normalization: bool = field( default=True, metadata={"help": "Whether to apply length normalization to the rewards."}, ) def __post_init__(self): # We do not use the post_init of GRPOConfig because: # 1. num_generations can be < 2 in MiniLLMConfig. Scale_rewards must be set to "none" to avoid nan. self.bf16 = not (self.fp16) if self.bf16 is None else self.bf16 TrainingArguments.__post_init__(self) self.scale_rewards = {True: "group", False: "none"}.get(self.scale_rewards, self.scale_rewards) if self.num_generations == 1: self.scale_rewards = "none" num_processes = self.world_size # The current default effective batch size if self.generation_batch_size is None and self.steps_per_generation is None: self.steps_per_generation = self.gradient_accumulation_steps self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation elif self.generation_batch_size is not None and self.steps_per_generation is None: # Just ensure the value is divisible by the global batch size if self.generation_batch_size % (self.per_device_train_batch_size * num_processes) != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by the global batch size " f"({self.per_device_train_batch_size * num_processes})." ) self.steps_per_generation = self.generation_batch_size // ( self.per_device_train_batch_size * num_processes ) elif self.generation_batch_size is None and self.steps_per_generation is not None: self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation else: raise ValueError( "'generation_batch_size' and 'steps_per_generation' can not be both configured at the same time" ) if self.do_eval and self.eval_strategy != "no": # Determine the number of generations to use for evaluation num_generations = self.num_generations_eval or self.num_generations # Just ensure the value is divisible by the global batch size if (self.per_device_eval_batch_size * num_processes) % num_generations != 0: raise ValueError( f"The global eval batch size ({self.per_device_eval_batch_size} * {num_processes}) must be " f"divisible by the number of generations used for evaluation ({num_generations})." ) # The generation batch must contain full prompt groups (no partials), so it must be divisible by # num_generations. if self.generation_batch_size % self.num_generations != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by num_generations " f"({self.num_generations})." ) if self.delta is not None and self.use_liger_kernel: raise ValueError("Liger kernel does not support two-sided GRPO loss yet.") ================================================ FILE: trl/experimental/minillm/minillm_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap import torch import torch.nn as nn import torch.nn.functional as F import transformers from datasets import Dataset, IterableDataset from packaging.version import Version from transformers import ( AutoModelForCausalLM, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, ) from transformers.utils import is_peft_available from ...models import prepare_deepspeed from ...trainer.grpo_trainer import GRPOTrainer, RewardFunc, RolloutFunc from ...trainer.utils import disable_dropout_in_model, get_config_model_id from ..utils import empty_cache from .minillm_config import MiniLLMConfig if is_peft_available(): from peft import PeftConfig def dummy_reward_func(completions: list, **kwargs): # placeholder reward function when no reward function is provided return [1.0 for _ in completions] class MiniLLMTrainer(GRPOTrainer): """ Trainer for the Knowledge Distillation of Language Models (MiniLLM) method. This algorithm was initially proposed in the paper [Knowledge Distillation of Large Language Models](https://huggingface.co/papers/2306.08543). Example: ```python from datasets import load_dataset from trl.experimental.minillm import MiniLLMTrainer dataset = load_dataset("trl-lib/tldr", split="train") trainer = MiniLLMTrainer( model="Qwen/Qwen3-0.6B", teacher_model="Qwen/Qwen3-1.7B", train_dataset=dataset, ) trainer.train() ``` Args: model (`str | PreTrainedModel`): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForCausalLM.from_pretrained`] with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. teacher_model (`PreTrainedModel | nn.Module | str`): Teacher model used for knowledge distillation. Instantiated similarly to `model`. reward_funcs (`RewardFunc | list[RewardFunc]`, *optional*): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function, such as: - A string: The *model ID* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForSequenceClassification.from_pretrained`] with `num_labels=1` and the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object: Only sequence classification models are supported. - A custom reward function: The function is provided with the prompts and the generated completions, plus any additional columns in the dataset. It should return a list of rewards. Custom reward functions can also return `None` when the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returns `None` for a sample, that reward function is excluded from the reward calculation for that sample. For more details, see [Using a custom reward function](#using-a-custom-reward-function). The trainer's state is also passed to the reward function. The trainer's state is an instance of [`~transformers.TrainerState`] and can be accessed by accessing the `trainer_state` argument to the reward function's signature. - A list of reward functions, where each item can independently be any of the above types. Mixing different types within the list (e.g., a string model ID and a custom reward function) is allowed. args ([`experimental.minillm.MiniLLMConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. It must include a column `"prompt"`. Any additional columns in the dataset is ignored. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. The padding side must be set to "left". If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. reward_processing_classes ([`~transformers.PreTrainedTokenizerBase`] or `list[PreTrainedTokenizerBase]`, *optional*): Processing classes corresponding to the reward functions specified in `reward_funcs`. Can be either: - A single processing class: Used when `reward_funcs` contains only one reward function. - A list of processing classes: Must match the order and length of the reward functions in `reward_funcs`. If set to `None`, or if an element of the list corresponding to a [`~transformers.PreTrainedModel`] is `None`, the tokenizer for the model is automatically loaded using [`~transformers.AutoTokenizer.from_pretrained`]. For elements in `reward_funcs` that are custom reward functions (not [`~transformers.PreTrainedModel`]), the corresponding entries in `reward_processing_classes` are ignored. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of [`AdamW`] on your model and a scheduler given by [`get_linear_schedule_with_warmup`] controlled by `args`. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. rollout_func (`RolloutFunc`, *optional*): Function to use for generating completions. It must take prompts, args, and processing_class as parameters and return a dict with `"prompt_ids"`, `"completion_ids"`, and `"logprobs"` fields. Any other fields that are forwarded to the reward functions. This feature is experimental and may change or be removed at any time without prior notice. """ _tag_names = ["trl", "minillm"] _name = "MiniLLM" _paper = { "title": "MiniLLM: Knowledge Distillation of Large Language Models", "id": "2306.08543", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{ gu2024minillm, title={{MiniLLM: Knowledge Distillation of Large Language Models}}, author={Yuxian Gu and Li Dong and Furu Wei and Minlie Huang}, booktitle={The Twelfth International Conference on Learning Representations}, year={2024}, url={https://openreview.net/forum?id=5h0qf7IBZZ} }"""), } def __init__( self, model: str | PreTrainedModel, teacher_model: PreTrainedModel | nn.Module | str, reward_funcs: RewardFunc | list[RewardFunc] | None = None, args: MiniLLMConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), peft_config: "PeftConfig | None" = None, rollout_func: RolloutFunc | None = None, ): if reward_funcs is None: reward_funcs = [dummy_reward_func] # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = MiniLLMConfig(f"{model_name}-MiniLLM") # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model, reward_funcs, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=reward_processing_classes, callbacks=callbacks, optimizers=optimizers, peft_config=peft_config, rollout_func=rollout_func, ) if args.teacher_model_init_kwargs is None: teacher_model_init_kwargs = {} elif not isinstance(teacher_model, str): raise ValueError( "You passed teacher_model_init_kwargs to the MiniLLMConfig, but your teacher_model is already instantiated." ) else: teacher_model_init_kwargs = args.teacher_model_init_kwargs teacher_model_init_kwargs["dtype"] = ( teacher_model_init_kwargs["dtype"] if teacher_model_init_kwargs["dtype"] in ["auto", None] else getattr(torch, teacher_model_init_kwargs["dtype"]) ) if isinstance(teacher_model, str): teacher_model = AutoModelForCausalLM.from_pretrained(teacher_model, **teacher_model_init_kwargs) # Disable dropout in the model if args.disable_dropout: disable_dropout_in_model(self.model) if self.is_deepspeed_enabled: self.teacher_model = prepare_deepspeed(teacher_model, self.accelerator) else: self.teacher_model = self.accelerator.prepare_model(teacher_model, evaluation_mode=True) self.temperature = args.temperature self.kd_temperature = args.kd_temperature self.single_step_decomposition = args.single_step_decomposition self.rkl_advantage = args.rkl_advantage self.gamma = args.gamma self.length_normalization = args.length_normalization def _single_step_decomposition_loss( self, student_log_probs: torch.Tensor, teacher_log_probs: torch.Tensor, mask: torch.Tensor | None = None, reduction: str = "batchmean", ): """ Compute the MiniLLM loss for knowledge distillation using F.kl_div. See Eq. (1) of https://huggingface.co/papers/2306.08543 for the definition. Args: student_logits: Tensor of shape (batch_size, sequence_length, vocab_size) teacher_logits: Tensor of shape (batch_size, sequence_length, vocab_size) labels: Tensor of shape (batch_size, sequence_length) with -100 for padding tokens to ignore when computing loss beta: Interpolation coefficient between 0 and 1 (default: 0.5) temperature: Softmax temperature (default: 1.0) reduction: Specifies the reduction to apply to the output (default: 'batchmean') Returns: loss: Scalar tensor with the generalized JSD loss """ reg_loss = F.kl_div( teacher_log_probs, student_log_probs, reduction="none", log_target=True ) # (batch_size, sequence_length) # Masking if mask is not None: reg_loss = reg_loss[mask] # Apply reduction if reduction == "batchmean": return reg_loss.sum() / mask.sum() if mask is not None else reg_loss.sum() / reg_loss.size(0) elif reduction == "sum": return reg_loss.sum() elif reduction == "mean": return reg_loss.mean() else: return reg_loss def _compute_advantage( self, student_log_probs_on_labels: torch.Tensor, teacher_log_probs_on_labels: torch.Tensor, mask: torch.Tensor | None = None, ) -> torch.Tensor: r"""Compute the advantage for Reverse KL Divergence. Mostly following [this implementation](https://github.com/microsoft/LMOps/blob/e210d2c026b9958617887762400778ace81172e6/minillm/minillm/losses.py#L37-L49). $$ \text{rewards}_t = \text{teacher\_log\_probs\_on\_labels}_t - \text{student\_log\_probs\_on\_labels}_t $$ If length normalization is enabled: $$ \text{lengths}_t = \sum_{i=t}^{T} \gamma^{i-t} $$ $$ \text{advantages}_t = \frac{\sum_{i=t}^{T} \gamma^{i-t} R_i}{\text{lengths}_t} $$ Otherwise: $$ \text{advantages}_t = \sum_{i=t}^{T} \gamma^{i-t} R_i $$ Args: student_log_probs_on_labels: Log probabilities of the student model on the labels. Shape: (batch_size, sequence_length) teacher_log_probs_on_labels: Log probabilities of the teacher model on the labels. Shape: (batch_size, sequence_length) mask: Optional mask to apply to the log probabilities. Shape: (batch_size, sequence_length) Returns: advantage: Computed advantage. Shape: (batch_size, sequence_length) """ response_length = student_log_probs_on_labels.size(1) if mask is None: mask = torch.ones_like(student_log_probs_on_labels) mask = mask.float() student_log_probs_on_labels = student_log_probs_on_labels * mask teacher_log_probs_on_labels = teacher_log_probs_on_labels * mask rewards = teacher_log_probs_on_labels - student_log_probs_on_labels # (batch_size, sequence_length) if self.gamma > 0.0: gamma_pow = torch.pow(self.gamma, torch.arange(response_length, device=rewards.device)) advantages = rewards * gamma_pow advantages = advantages.flip(1).cumsum(dim=1).flip(1) if self.length_normalization: mask = torch.where(mask < 0.5, 1e-4, mask) lengths = mask * gamma_pow lengths = lengths.flip(1).cumsum(dim=1).flip(1) advantages = advantages / lengths else: advantages = rewards return advantages def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): input_ids = torch.cat([inputs["prompt_ids"], inputs["completion_ids"]], dim=1) attention_mask = torch.cat([inputs["prompt_mask"], inputs["completion_mask"]], dim=1) labels = input_ids.clone() labels[attention_mask == 0] = -100 # Compute student output student_outputs = model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False) # Compute teacher output in eval mode self.teacher_model.eval() with torch.no_grad(): teacher_outputs = self.teacher_model(input_ids=input_ids, attention_mask=attention_mask, use_cache=False) # Slice the logits for the generated tokens using the inputs["prompts"] lengths prompt_lengths = inputs["prompt_ids"].shape[1] student_logits = student_outputs.logits[:, prompt_lengths - 1 : -1, :] teacher_logits = teacher_outputs.logits[:, prompt_lengths - 1 : -1, :] shifted_labels = input_ids[:, prompt_lengths:] # Apply temperature scaling student_logits = student_logits / self.kd_temperature teacher_logits = teacher_logits / self.kd_temperature # Compute log probabilities for student and probabilities for teacher student_log_probs = F.log_softmax(student_logits, dim=-1) teacher_log_probs = F.log_softmax(teacher_logits, dim=-1) student_log_probs_on_labels = torch.gather( student_log_probs, dim=-1, index=shifted_labels.unsqueeze(-1) ).squeeze(-1) teacher_log_probs_on_labels = torch.gather( teacher_log_probs, dim=-1, index=shifted_labels.unsqueeze(-1) ).squeeze(-1) mask = shifted_labels != -100 if self.rkl_advantage: reverse_kl_advantage = self._compute_advantage( student_log_probs_on_labels=student_log_probs_on_labels, teacher_log_probs_on_labels=teacher_log_probs_on_labels, mask=mask, ) inputs["advantages"] = inputs["advantages"].unsqueeze(1) + reverse_kl_advantage # Compute GRPO loss on verifiable reward loss = self._compute_loss(model, inputs) # Compute loss if self.single_step_decomposition: single_step_decomposition_loss = self._single_step_decomposition_loss( student_log_probs=student_log_probs, teacher_log_probs=teacher_log_probs, mask=mask, ) loss += single_step_decomposition_loss # Empty cache empty_cache() # Return loss return (loss, student_outputs) if return_outputs else loss ================================================ FILE: trl/experimental/nash_md/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .nash_md_config import NashMDConfig from .nash_md_trainer import NashMDTrainer __all__ = ["NashMDConfig", "NashMDTrainer"] ================================================ FILE: trl/experimental/nash_md/nash_md_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from ..online_dpo import OnlineDPOConfig @dataclass class NashMDConfig(OnlineDPOConfig): r""" Configuration class for the [`experimental.nash_md.NashMDTrainer`]. Subclass of [`experimental.online_dpo.OnlineDPOConfig`] we can use all its arguments and add the following: Parameters: mixture_coef (`float` or `list[float]`, *optional*, defaults to `0.5`): Logit mixture coefficient for the model and reference model. If a list of floats is provided then the mixture coefficient is selected for each new epoch and the last coefficient is used for the rest of the epochs. """ mixture_coef: list[float] = field( default_factory=lambda: [0.5], metadata={ "help": "Logit mixture coefficient for the model and reference model. If a list of floats is provided " "then the mixture coefficient is selected for each new epoch and the last coefficient is used for the " "rest of the epochs." }, ) def __post_init__(self): super().__post_init__() if hasattr(self.mixture_coef, "__len__") and len(self.mixture_coef) == 1: self.mixture_coef = self.mixture_coef[0] ================================================ FILE: trl/experimental/nash_md/nash_md_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap from collections.abc import Callable from typing import Any import jinja2 import torch import torch.nn as nn import torch.nn.functional as F from datasets import Dataset, IterableDataset from transformers import ( BaseImageProcessor, FeatureExtractionMixin, GenerationMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, ) from transformers.trainer_utils import EvalPrediction from transformers.training_args import OptimizerNames from transformers.utils import is_peft_available from ...data_utils import is_conversational, maybe_apply_chat_template from ...models.utils import unwrap_model_for_generation from ...trainer.utils import selective_log_softmax from ..judges import BasePairwiseJudge from ..online_dpo import OnlineDPOTrainer from ..utils import SIMPLE_CHAT_TEMPLATE, empty_cache, get_reward, truncate_right from .nash_md_config import NashMDConfig if is_peft_available(): from peft import PeftModel class GeometricMixtureWrapper(GenerationMixin): """ Geometric Mixture generation wrapper that samples from the logits of two model's geometric mixture. Args: model ([`~transformers.PreTrainedModel`]): The model to be wrapped. ref_model ([`~transformers.PreTrainedModel`]): The reference model. generation_config ([`~transformers.GenerationConfig`]): The generation config. mixture_coef (`float`, *optional* - default: 0.5): The mixture coefficient. """ main_input_name = "input_ids" _supports_cache_class = False _supports_static_cache = False _is_stateful = False def __init__(self, model, ref_model, generation_config, mixture_coef=0.5, device=None): super().__init__() self.model = model self.config = model.config self.ref_model = ref_model self.generation_config = generation_config self.mixture_coef = mixture_coef self.device = device if hasattr(self.model, "_is_stateful"): self._is_stateful = self.model._is_stateful def __call__(self, *args, **kwargs): return self.forward(*args, **kwargs) @torch.inference_mode() def forward(self, *args, **kwargs): model_outputs = self.model(*args, **kwargs) model_logits = model_outputs.logits ref_model_logits = self.ref_model(*args, **kwargs).logits model_outputs.logits = torch.nn.functional.log_softmax( self.mixture_coef * ref_model_logits + (1 - self.mixture_coef) * model_logits, dim=-1 ) return model_outputs def prepare_inputs_for_generation(self, *args, **kwargs): # turn off cache in the generation config kwargs["use_cache"] = False model_inputs = self.model.prepare_inputs_for_generation(*args, **kwargs) _ = self.ref_model.prepare_inputs_for_generation(*args, **kwargs) return model_inputs def _validate_model_class(self): self.model._validate_model_class() def _validate_model_kwargs(self, model_kwargs): return self.model._validate_model_kwargs(model_kwargs) class NashMDTrainer(OnlineDPOTrainer): """ Trainer for the Nash-MD method. It is implemented as a subclass of [`experimental.online_dpo.OnlineDPOTrainer`]. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an `AutoModelForCausalLM`. ref_model ([`~transformers.PreTrainedModel`]): Hugging Face transformer model with a casual language modelling head. Used for implicit reward computation and loss. If no reference model is provided, the trainer will create a reference model with the same architecture as the model to be optimized. reward_funcs ([`~transformers.PreTrainedModel`]): The reward model to score completions with, preferably an [`~transformers.AutoModelForSequenceClassification`]. judge ([`experimental.judges.BasePairwiseJudge`]): The judge to use for pairwise comparison of model completions. args ([`experimental.nash_md.NashMDConfig`]): The NashMD config arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. peft_config (`dict`): The peft config to use for training. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. """ _tag_names = ["trl", "nash-md"] _name = "Nash-MD" _paper = { "title": "Nash Learning from Human Feedback", "id": "2312.00886", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{munos2024nash, title = {{Nash Learning from Human Feedback}}, author = {R{\'{e}}mi Munos and Michal Valko and Daniele Calandriello and Mohammad Gheshlaghi Azar and Mark Rowland and Zhaohan Daniel Guo and Yunhao Tang and Matthieu Geist and Thomas Mesnard and C{\\^{o}}me Fiegel and Andrea Michi and Marco Selvi and Sertan Girgin and Nikola Momchev and Olivier Bachem and Daniel J. Mankowitz and Doina Precup and Bilal Piot}, year = 2024, booktitle = {Forty-first International Conference on Machine Learning, {ICML} 2024, Vienna, Austria, July 21-27, 2024}, publisher = {OpenReview.net}, url = {https://openreview.net/forum?id=Y5AmNYiyCQ} }"""), } def __init__( self, model: PreTrainedModel | nn.Module = None, ref_model: PreTrainedModel | nn.Module = None, reward_funcs: PreTrainedModel | nn.Module | None = None, judge: BasePairwiseJudge | None = None, args: NashMDConfig | None = None, data_collator: Callable | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, ) -> None: super().__init__( model=model, ref_model=ref_model, reward_funcs=reward_funcs, judge=judge, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=processing_class, peft_config=peft_config, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) self._mixture_coef = self.args.mixture_coef # Overwrite the stats dictionary to include NashMD specific statistics self.stats = { # Remove "non_score_reward", "rlhf_reward", "scores_margin" # Add "mixture_coef" "loss/kl": [], "objective/entropy": [], "loss/score": [], "rewards/probabilities": [], "rewards/accuracies": [], "rewards/margins": [], "logps/chosen": [], "logps/rejected": [], "val/model_contain_eos_token": [], "val/ref_contain_eos_token": [], "beta": [], "mixture_coef": [], } if self.reward_funcs is not None: if len(self.reward_funcs) != 1: raise ValueError("NashMDTrainer only supports one reward function/model.") self.reward_funcs = self.reward_funcs[0] self.stats["rewards/chosen"] = [] self.stats["rewards/rejected"] = [] @property def mixture_coef(self): if isinstance(self._mixture_coef, list): epoch = self.state.epoch return self._mixture_coef[epoch] if epoch < len(self._mixture_coef) else self._mixture_coef[-1] else: return self._mixture_coef def _generate_completions(self, model, prompts): # Generate completions from the policy model. with ( unwrap_model_for_generation( model, self.accelerator, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_policy_for_gen_ctx ): model_output = unwrapped_policy_for_gen_ctx.generate( input_ids=prompts["input_ids"], attention_mask=prompts["attention_mask"], generation_config=self.generation_config, ) # Get the DDP/FSDP unwrapped version of the main model. # This will be the policy model for GeometricMixtureWrapper (PEFT adapters active if PEFT is used). policy_model_for_gmw = self.accelerator.unwrap_model(model) # Determine the correct reference model for GeometricMixtureWrapper. # This also needs to be DDP/FSDP unwrapped. ref_model_for_gmw: torch.nn.Module if self.ref_model is None: # No explicit ref_model is provided. # Use the base of the main `model` if it's a PEFT model. # policy_model_for_gmw is already DDP-unwrapped. if is_peft_available() and isinstance(policy_model_for_gmw, PeftModel): ref_model_for_gmw = policy_model_for_gmw.get_base_model() else: # Not a PEFT model (or PEFT not available), or already a base model. # Use the DDP-unwrapped policy model itself as the reference. ref_model_for_gmw = policy_model_for_gmw else: # An explicit ref_model is provided. Unwrap it for DDP/FSDP. ref_model_for_gmw = self.accelerator.unwrap_model(self.ref_model) # Both models given to GeometricMixtureWrapper (policy_model_for_gmw and ref_model_for_gmw) are DDP-unwrapped. with torch.no_grad(): # Ensure no_grad context for mixture model generation mixture_model = GeometricMixtureWrapper( model=policy_model_for_gmw, ref_model=ref_model_for_gmw, generation_config=self.generation_config, mixture_coef=self.mixture_coef, device=self.accelerator.device, ) # TODO: use self._override_model_generation_config for both models? mixture_output = mixture_model.generate( input_ids=prompts["input_ids"], attention_mask=prompts["attention_mask"], generation_config=self.generation_config, ) return model_output, mixture_output def _process_completions(self, model_output, mixture_output, prompts): context_length = prompts["input_ids"].shape[1] # Process model completions model_completion_ids = model_output[:, context_length:] model_completion_ids, model_completion_mask = truncate_right( model_completion_ids, self.processing_class.eos_token_id, self.processing_class.pad_token_id ) model_data = { "input_ids": torch.cat((prompts["input_ids"], model_completion_ids), dim=1), "attention_mask": torch.cat((prompts["attention_mask"], model_completion_mask), dim=1), "raw": prompts["raw"], } # Process reference model completions mixture_completion_ids = mixture_output[:, context_length:] mixture_completion_ids, mixture_completion_mask = truncate_right( mixture_completion_ids, self.processing_class.eos_token_id, self.processing_class.pad_token_id ) mixture_data = { "input_ids": torch.cat((prompts["input_ids"], mixture_completion_ids), dim=1), "attention_mask": torch.cat((prompts["attention_mask"], mixture_completion_mask), dim=1), "raw": prompts["raw"], } return model_data, mixture_data def _compute_rewards(self, model_data, mixture_data, context_length): with torch.no_grad(): _, model_scores, _ = get_reward( self.reward_funcs, model_data["input_ids"], self.processing_class.pad_token_id, context_length ) _, mixture_scores, _ = get_reward( self.reward_funcs, mixture_data["input_ids"], self.processing_class.pad_token_id, context_length ) # Apply EOS penalty if needed if self.args.missing_eos_penalty is not None: model_contain_eos = torch.any(model_data["input_ids"] == self.processing_class.eos_token_id, dim=-1) mixture_contain_eos = torch.any(mixture_data["input_ids"] == self.processing_class.eos_token_id, dim=-1) model_scores[~model_contain_eos] -= self.args.missing_eos_penalty mixture_scores[~mixture_contain_eos] -= self.args.missing_eos_penalty return model_scores, mixture_scores def _compute_judge(self, model_data, mixture_data, context_length): prompts = model_data["raw"] model_data_completions = self.processing_class.batch_decode( model_data["input_ids"][:, context_length:], skip_special_tokens=True ) model_data_completions = [completion.strip() for completion in model_data_completions] mixture_data_completions = self.processing_class.batch_decode( mixture_data["input_ids"][:, context_length:], skip_special_tokens=True ) mixture_data_completions = [completion.strip() for completion in mixture_data_completions] if is_conversational({"prompt": prompts[0]}): model_data_completions = [ [{"role": "assistant", "content": completion}] for completion in model_data_completions ] environment = jinja2.Environment() template = environment.from_string(SIMPLE_CHAT_TEMPLATE) prompts = [template.render(messages=message) for message in prompts] model_data_completions = [template.render(messages=completion) for completion in model_data_completions] mixture_data_completions = [ [{"role": "assistant", "content": completion}] for completion in mixture_data_completions ] mixture_data_completions = [ template.render(messages=completion) for completion in mixture_data_completions ] probability = self.judge.judge( prompts, list(zip(model_data_completions, mixture_data_completions, strict=True)), return_scores=True, ) return torch.tensor(probability, device=model_data["input_ids"].device) def _compute_logprobs(self, model, model_data, context_length): def compute_logprobs_for_data(m, data): output = m(data["input_ids"], attention_mask=data["attention_mask"]) logits = output.logits[:, context_length - 1 : -1] token_logprobs = selective_log_softmax(logits, data["input_ids"][:, context_length:]) return token_logprobs # Compute logprobs for model completions under the model model_logprobs_model_data = compute_logprobs_for_data(model, model_data) # Compute logprobs of model completions under the reference model with torch.no_grad(): if self.ref_model is None: with model.disable_adapter(): ref_logprobs_model_data = compute_logprobs_for_data(model, model_data) else: ref_logprobs_model_data = compute_logprobs_for_data(self.ref_model, model_data) # Mask padding tokens model_padding_mask = model_data["attention_mask"][:, context_length:] == 0 model_logprobs_model_data = model_logprobs_model_data.masked_fill(model_padding_mask, 0.0) ref_logprobs_model_data = ref_logprobs_model_data.masked_fill(model_padding_mask, 0.0) return (model_logprobs_model_data, ref_logprobs_model_data) def _compute_losses( self, model_logprobs_model_data, ref_logprobs_model_data, probability, ): # reinforce score where 0.5 is a control variate score = (probability - 0.5) * model_logprobs_model_data.sum(1) # kl divergence via reinforce with torch.no_grad(): log_ratio = model_logprobs_model_data - ref_logprobs_model_data kl_div_log = log_ratio.sum(1) kl_div_loss = (log_ratio * model_logprobs_model_data).sum(1) # final loss loss = self.beta * kl_div_loss - score return loss.mean(), score, kl_div_log def _log_statistics( self, model_data, mixture_data, model_logprobs_model_data, ref_logprobs_model_data, probability, score, kl_div, context_length, model_scores=None, mixture_scores=None, ): # Helper function to gather and compute mean def gather_mean(tensor): return self.accelerator.gather_for_metrics(tensor).mean().item() # Log score self.stats["loss/score"].append(gather_mean(score)) # Log KL divergence self.stats["loss/kl"].append(gather_mean(kl_div)) # Log logprobs model_logprobs_model_data_sum = model_logprobs_model_data.sum(1) ref_logprobs_model_data_sum = ref_logprobs_model_data.sum(1) self.stats["logps/chosen"].append(gather_mean(model_logprobs_model_data_sum)) self.stats["logps/rejected"].append(gather_mean(ref_logprobs_model_data_sum)) # Log rewards if self.reward_funcs is not None: self.stats["rewards/chosen"].append(gather_mean(model_scores)) self.stats["rewards/rejected"].append(gather_mean(mixture_scores)) # Log probabilities self.stats["rewards/probabilities"].append(gather_mean(probability)) # Calculate entropy for model data entropy_model_data = -model_logprobs_model_data.sum(1) self.stats["objective/entropy"].append(gather_mean(entropy_model_data)) # Calculate margins margin = model_logprobs_model_data_sum - ref_logprobs_model_data_sum self.stats["rewards/margins"].append(gather_mean(margin)) # Calculate accuracy accuracy = (margin > 0).float() self.stats["rewards/accuracies"].append(gather_mean(accuracy)) # Log EOS token statistics model_eos = (model_data["input_ids"][:, context_length:] == self.processing_class.eos_token_id).any(dim=1) mixture_eos = (mixture_data["input_ids"][:, context_length:] == self.processing_class.eos_token_id).any(dim=1) self.stats["val/model_contain_eos_token"].append(gather_mean(model_eos.float())) self.stats["val/ref_contain_eos_token"].append(gather_mean(mixture_eos.float())) # Log beta and mixture coef self.stats["beta"].append(self.beta) self.stats["mixture_coef"].append(self.mixture_coef) def training_step( self, model: nn.Module, inputs: dict[str, torch.Tensor | Any], num_items_in_batch: int | None = None ) -> torch.Tensor: model.train() # Apply chat template and tokenize the input batch_size = len(next(iter(inputs.values()))) prompts = inputs["prompt"] inputs = [{k: v[i] for k, v in inputs.items()} for i in range(batch_size)] inputs = [maybe_apply_chat_template(x, self.processing_class) for x in inputs] inputs = [self.tokenize_row(x, self.model.config.is_encoder_decoder, self.processing_class) for x in inputs] inputs = self.data_collator(inputs) # need the prompt_ only inputs = self._prepare_inputs(inputs) context_length = inputs["prompt_input_ids"].shape[1] prompts = { "input_ids": inputs["prompt_input_ids"], "attention_mask": inputs["prompt_attention_mask"], "raw": prompts, } del inputs # Sample completions from both the model and the reference model model_output, mixture_output = self._generate_completions(model, prompts) # Process model completions model_data, mixture_data = self._process_completions(model_output, mixture_output, prompts) # Compute rewards if self.reward_funcs is not None: model_scores, mixture_scores = self._compute_rewards(model_data, mixture_data, context_length) # probability of the model data vs the mixture data probability = F.sigmoid(model_scores - mixture_scores) else: model_scores, mixture_scores = None, None probability = self._compute_judge(model_data, mixture_data, context_length) # Compute logprobs model_logprobs_model_data, ref_logprobs_model_data = self._compute_logprobs(model, model_data, context_length) # Compute loss loss, score, kl_div = self._compute_losses(model_logprobs_model_data, ref_logprobs_model_data, probability) # Log everything self._log_statistics( model_data, mixture_data, model_logprobs_model_data.detach(), ref_logprobs_model_data, probability, score.detach(), kl_div.detach(), context_length, model_scores, mixture_scores, ) if ( self.args.torch_empty_cache_steps is not None and self.state.global_step % self.args.torch_empty_cache_steps == 0 ): empty_cache() kwargs = {} # For LOMO optimizers you need to explicitly use the learning rate if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: kwargs["learning_rate"] = self._get_learning_rate() if self.args.n_gpu > 1: loss = loss.mean() # mean() to average on multi-gpu parallel training self.accelerator.backward(loss, **kwargs) return loss.detach() / self.args.gradient_accumulation_steps ================================================ FILE: trl/experimental/online_dpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .online_dpo_config import OnlineDPOConfig from .online_dpo_trainer import OnlineDPOTrainer __all__ = ["OnlineDPOConfig", "OnlineDPOTrainer"] ================================================ FILE: trl/experimental/online_dpo/online_dpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from dataclasses import dataclass, field from typing import Any from ...trainer.base_config import _BaseConfig @dataclass class OnlineDPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.online_dpo.OnlineDPOTrainer`]. This class includes only the parameters that are specific to Online DPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: reward_model_path (`str`, *optional*): Path to the reward model. Either `judge` or `reward_model_path` must be set, but not both. judge (`str`, *optional*): Name of the judge to use. Either `judge` or `reward_model_path` must be set, but not both. max_new_tokens (`int`, *optional*, defaults to `64`): Maximum number of tokens to generate per completion. max_length (`int`, *optional*, defaults to `256`): Maximum total length of the sequence (prompt + completion) used to compute log probabilities. If the sequence exceeds this limit, the leftmost tokens will be truncated to preserve as much of the completion as possible. temperature (`float`, *optional*, defaults to `0.9`): Temperature for sampling. The higher the temperature, the more random the completions. missing_eos_penalty (`float`, *optional*): Penalty applied to the score when the model fails to generate an EOS token. This is useful to encourage to generate completions shorter than the maximum length (`max_new_tokens`). The penalty must be a positive value. This parameter only works when using `reward_funcs` and not when using `judge`. beta (`float` or `list[float]`, *optional*, defaults to `0.1`): Parameter controlling the deviation from the reference model. Higher β means less deviation from the reference model. For the IPO loss (`loss_type="ipo"`), β is the regularization parameter denoted by τ in the [paper](https://huggingface.co/papers/2310.12036). If a list of floats is provided then the β is selected for each new epoch and the last β is used for the rest of the epochs. loss_type (`str`, *optional*, defaults to `"sigmoid"`): Type of loss to use. Possible values are: - `"sigmoid"`: sigmoid loss from the original [DPO](https://huggingface.co/papers/2305.18290) paper. - `"ipo"`: IPO loss from the [IPO](https://huggingface.co/papers/2310.12036) paper. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model and reference model. > Parameters that control generation top_p (`float`, *optional*, defaults to `1.0`): Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to `1.0` to consider all tokens. top_k (`int`, *optional*, defaults to `0`): Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, top-k-filtering is disabled and all tokens are considered. min_p (`float`, *optional*): Minimum token probability, which will be scaled by the probability of the most likely token. It must be a value between `0.0` and `1.0`. Typical values are in the `0.01-0.2` range. repetition_penalty (`float`, *optional*, defaults to `1.0`): Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > `1.0` encourage the model to use new tokens, while values < `1.0` encourage the model to repeat tokens. use_transformers_paged (`bool`, *optional*, defaults to `False`): Whether to use the `transformers` paged implementation for generation. If set to `True`, the `transformers` paged implementation will be used for generation instead of the default padded implementation. This parameter is only effective when `use_vllm` is set to `False`. cache_implementation (`str`, *optional*): Implementation of the cache method for faster generation when `use_vllm` is set to `False`. generation_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to [`~transformers.GenerationConfig`] (if using transformers) or `SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them. > Parameters that control generation acceleration powered by vLLM use_vllm (`bool`, *optional*, defaults to `False`): Whether to use vLLM for generating completions. If set to `True`, the trainer will use vLLM for generation instead of the default model.generate(). Requires `vllm` to be installed. vllm_model_impl (`str`, *optional*, defaults to `"vllm"`): Model implementation to use for vLLM. Must be one of `"transformers"` or `"vllm"`. `"transformers"`: Use the `transformers` backend for model implementation. `"vllm"`: Use the `vllm` library for model implementation. vllm_mode (`str`, *optional*, defaults to `"colocate"`): Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `"server"` or `"colocate"`. - `"server"`: The trainer will send generation requests to a separate vLLM server. Make sure a TRL vLLM server is running (start with `trl vllm-serve`). - `"colocate"`: vLLM will run in the same process and share the training GPUs. This avoids the need for a separate server but may cause resource contention with training. vllm_structured_outputs_regex (`str`, *optional*): Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled. > Parameters that control the vLLM server (only used when `vllm_mode` is `"server"`) vllm_server_base_url (`str`, *optional*): Base URL for the vLLM server (e.g., `"http://localhost:8000"`). If provided, `vllm_server_host` and `vllm_server_port` are ignored. vllm_server_host (`str`, *optional*, defaults to `"0.0.0.0"`): Host of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_port (`int`, *optional*, defaults to `8000`): Port of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_timeout (`float`, *optional*, defaults to `240.0`): Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up after the timeout, a `ConnectionError` is raised. vllm_group_port (`int`, *optional*, defaults to `51216`): Port number for the weight update group. This is used to communicate with the vLLM server. Unless the port is occupied, there is no need to change it. > Parameters that control colocated vLLM execution (only used when `vllm_mode` is `"colocate"`) vllm_gpu_memory_utilization (`float`, *optional*, defaults to `0.55`): Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_gpu_memory_utilization` flag. vllm_tensor_parallel_size (`int`, *optional*, defaults to `1`): Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_tensor_parallel_size` flag. vllm_enable_sleep_mode (`bool`, *optional*, defaults to `False`): Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory usage low, but waking the engine adds host–device transfer latency. > Other parameters ds3_gather_for_generation (`bool`, *optional*, defaults to `True`): This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for generation, improving generation speed. However, disabling this option allows training models that exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option is not compatible with vLLM generation. model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model from a string. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `5e-7` instead of `5e-5`. > - `remove_unused_columns`: Defaults to `False` instead of `True`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=5e-7, metadata={"help": "The initial learning rate for AdamW."}, ) remove_unused_columns: bool = field( default=False, metadata={"help": "Whether or not to automatically remove the columns unused by the model forward method."}, ) reward_model_path: str | None = field( default=None, metadata={ "help": "Path to the reward model. Either `judge` or `reward_model_path` must be set, but not both." }, ) judge: str | None = field( default=None, metadata={ "help": "Name of the judge to use. Either `judge` or `reward_model_path` must be set, but not both." }, ) max_new_tokens: int = field( default=64, metadata={"help": "Maximum number of tokens to generate per completion."}, ) max_length: int = field( default=512, metadata={ "help": "Maximum total length of the sequence (prompt + completion) used to compute log probabilities. If " "the sequence exceeds this limit, the leftmost tokens will be truncated to preserve as much of the " "completion as possible." }, ) temperature: float = field( default=0.9, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) top_p: float = field( default=1.0, metadata={ "help": "Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. " "Set to 1.0 to consider all tokens." }, ) top_k: int = field( default=0, metadata={ "help": "Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, " "top-k-filtering is disabled and all tokens are considered." }, ) min_p: float | None = field( default=None, metadata={ "help": "Minimum token probability, which will be scaled by the probability of the most likely token. It " "must be a value between 0.0 and 1.0. Typical values are in the 0.01-0.2 range." }, ) repetition_penalty: float = field( default=1.0, metadata={ "help": "Float that penalizes new tokens based on whether they appear in the prompt and the generated " "text so far. Values > 1.0 encourage the model to use new tokens, while values < 1.0 encourage the model " "to repeat tokens." }, ) generation_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to `GenerationConfig` (if using transformers) or " "`SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the " "generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that " "conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them." }, ) use_transformers_paged: bool = field( default=False, metadata={ "help": "Whether to use the `transformers` paged implementation for generation. If set to `True`, the " "`transformers` paged implementation will be used for generation instead of the default padded " "implementation. This parameter is only effective when `use_vllm` is set to `False`." }, ) cache_implementation: str | None = field( default=None, metadata={"help": "Implementation of the cache method for faster generation when use_vllm is set to False."}, ) missing_eos_penalty: float | None = field( default=None, metadata={ "help": "Penalty applied to the score when the model fails to generate an EOS token. This is useful to " "encourage to generate completions shorter than the maximum length (`max_new_tokens`). The penalty must be " "a positive value." }, ) beta: list[float] = field( default_factory=lambda: [0.1], metadata={ "help": "Parameter controlling the deviation from the reference model. Higher β means less deviation from " "the reference model. For the IPO loss (`loss_type='ipo'`), β is the regularization parameter denoted by " "τ in the [paper](https://huggingface.co/papers/2310.12036). If a list of floats is provided then the β " "is selected for each new epoch and the last β is used for the rest of the epochs." }, ) loss_type: str = field( default="sigmoid", metadata={ "help": "Type of loss to use.", "choices": ["sigmoid", "ipo"], }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model."}, ) use_vllm: bool = field( default=False, metadata={ "help": "Whether to use vLLM for generating completions. Requires vLLM to be installed " "(`pip install trl[vllm]`)." }, ) vllm_model_impl: str = field( default="vllm", metadata={ "help": "Model implementation to use for vLLM. Must be one of `transformers` or `vllm`. `transformers`: " "Use the `transformers` backend for model implementation. `vllm`: Use the `vllm` library for " "model implementation." }, ) vllm_structured_outputs_regex: str | None = field( default=None, metadata={"help": "Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled."}, ) vllm_gpu_memory_utilization: float | None = field( default=0.55, metadata={ "help": "Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_gpu_memory_utilization` flag.", }, ) vllm_mode: str = field( default="colocate", metadata={ "help": "Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `'server'` or " "`'colocate'`. `'server'`: The trainer will send generation requests to a separate vLLM server. Make sure " "a TRL vLLM server is running (start with `trl vllm-serve`). `'colocate'`: vLLM will run in the same " "process and share the training GPUs. This avoids the need for a separate server but may cause resource " "contention with training.", }, ) vllm_server_base_url: str | None = field( default=None, metadata={ "help": "Base URL for the vLLM server (e.g., 'http://localhost:8000'). If provided, `vllm_server_host` " "and `vllm_server_port` are ignored.", }, ) vllm_server_host: str = field( default="0.0.0.0", metadata={"help": "Host of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_port: int = field( default=8000, metadata={"help": "Port of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_timeout: float = field( default=240.0, metadata={ "help": "Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up " "after the timeout, a `ConnectionError` is raised.", }, ) vllm_group_port: int = field( default=51216, metadata={ "help": "Port number for the weight update group. This is used to communicate with the vLLM server. " "Unless the port is occupied, there is no need to change it.", }, ) vllm_tensor_parallel_size: int = field( default=1, metadata={ "help": "Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_tensor_parallel_size` flag.", }, ) vllm_enable_sleep_mode: bool = field( default=False, metadata={ "help": "Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory " "usage low, but waking the engine adds host–device transfer latency." }, ) ds3_gather_for_generation: bool = field( default=True, metadata={ "help": "This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for " "generation, improving generation speed. However, disabling this option allows training models that " "exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option " "is not compatible with vLLM generation." }, ) model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model " "from a string." }, ) reward_weights: list[float] | None = field( default=None, metadata={ "help": "Weights for combining multiple reward functions. Must match the number of reward functions. " "If None, all reward functions are equally weighted." }, ) def __post_init__(self): super().__post_init__() if hasattr(self.beta, "__len__") and len(self.beta) == 1: self.beta = self.beta[0] if self.max_new_tokens >= self.max_length: warnings.warn( f"The configuration has `max_new_tokens` ({self.max_new_tokens}) >= `max_length` ({self.max_length}). " "This will cause prompts to be truncated or completely removed in the forward pass. " "To preserve prompts, ensure e.g. `max_length > max_new_tokens + 512`. ", stacklevel=3, ) ================================================ FILE: trl/experimental/online_dpo/online_dpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import re import textwrap from collections.abc import Callable from contextlib import nullcontext from pathlib import Path from typing import Any import jinja2 import torch import torch.nn as nn import torch.nn.functional as F import torch.utils.data import transformers from accelerate import logging from accelerate.utils import broadcast_object_list, gather_object, is_peft_model from datasets import Dataset from packaging.version import Version from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.utils.data import IterableDataset from transformers import ( AutoModelForCausalLM, AutoModelForSequenceClassification, AutoTokenizer, DataCollator, GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, is_bitsandbytes_available, ) from transformers.models.auto.modeling_auto import MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES from transformers.trainer_utils import EvalPrediction from transformers.training_args import OptimizerNames from transformers.utils import is_flash_attn_2_available, is_peft_available, is_sagemaker_mp_enabled from ...data_utils import apply_chat_template, is_conversational, maybe_apply_chat_template from ...extras.profiling import profiling_context from ...generation.vllm_client import VLLMClient from ...import_utils import is_vllm_available from ...models.utils import prepare_deepspeed, prepare_fsdp, unwrap_model_for_generation from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import disable_dropout_in_model, ensure_master_addr_port, get_config_model_id, pad from ..judges import BasePairwiseJudge from ..utils import ( SIMPLE_CHAT_TEMPLATE, DPODataCollatorWithPadding, create_reference_model, empty_cache, prepare_peft_model, truncate_right, ) from .online_dpo_config import OnlineDPOConfig if is_peft_available(): from peft import PeftConfig, PeftModel if is_sagemaker_mp_enabled(): from smdistributed.modelparallel import __version__ as SMP_VERSION IS_SAGEMAKER_MP_POST_1_10 = Version(SMP_VERSION) >= Version("1.10") else: IS_SAGEMAKER_MP_POST_1_10 = False if Version(transformers.__version__) >= Version("5.2.0"): from transformers.trainer_pt_utils import nested_gather if is_vllm_available(): from vllm import LLM, SamplingParams from vllm.sampling_params import StructuredOutputsParams if is_bitsandbytes_available(): import bitsandbytes as bnb logger = logging.get_logger(__name__) # A reward function can be a string, interpreted as a model ID and loaded as a pretrained model, a pretrained model, or # a callable that returns a list of floats (the rewards). The callable receives prompts, completions, and additional # arguments from the trainer (refer to the trainer's source for details). To ensure forward compatibility, it should # accept **kwargs. RewardFunc = str | PreTrainedModel | Callable[..., list[float | None]] class OnlineDPOTrainer(_BaseTrainer): r""" Initialize OnlineDPOTrainer. Args: model (`str | nn.Module | PreTrainedModel`): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForCausalLM.from_pretrained`] with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. ref_model ([`~transformers.PreTrainedModel`] or `torch.nn.Module` or `None`): The reference model to use for training. If None is specified, the reference model will be created from the model. judge ([`experimental.judges.BasePairwiseJudge`]): The judge to use for pairwise comparison of model completions. reward_funcs (`RewardFunc | list[RewardFunc]`, *optional*): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function: Can be a string (path to model), a [`~transformers.PreTrainedModel`], or a custom callable function. - A list of reward functions: Must all be of compatible types. Note: Only one of `judge`, or `reward_funcs` should be provided. args ([`experimental.online_dpo.OnlineDPOConfig`]): The online DPO config arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. reward_processing_classes ([`~transformers.PreTrainedTokenizerBase`] or `list[PreTrainedTokenizerBase]`, *optional*): Processing classes corresponding to the reward functions specified in `reward_funcs`. Can be either: - A single processing class: Used when `reward_funcs` contains only one reward function. - A list of processing classes: Must match the order and length of the reward functions in `reward_funcs`. If set to `None`, the tokenizer for each model-based reward function is automatically loaded using [`~transformers.AutoTokenizer.from_pretrained`]. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. """ _tag_names = ["trl", "online-dpo"] _name = "Online DPO" _paper = { "title": "Direct Language Model Alignment from Online AI Feedback", "id": "2402.04792", # docstyle-ignore "citation": textwrap.dedent("""\ @article{guo2024direct, title = {{Direct Language Model Alignment from Online AI Feedback}}, author = {Shangmin Guo and Biao Zhang and Tianlin Liu and Tianqi Liu and Misha Khalman and Felipe Llinares and Alexandre Ram{\'{e}} and Thomas Mesnard and Yao Zhao and Bilal Piot and Johan Ferret and Mathieu Blondel}, year = 2024, eprint = {arXiv:2402.04792} }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str, ref_model: PreTrainedModel | nn.Module | None = None, reward_funcs: RewardFunc | list[RewardFunc] | None = None, judge: BasePairwiseJudge | None = None, args: OnlineDPOConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, peft_config: "PeftConfig | None" = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, ) -> None: if train_dataset is None: raise ValueError("`train_dataset` is required") if ref_model is model: raise ValueError( "`model` and `ref_model` cannot be the same object. If you want `ref_model` to be the " "same as `model`, either omit the `ref_model` argument or pass `None`." ) self.ref_model = ref_model # Validate reward configuration - must have exactly one of: judge, or reward_funcs reward_configs = sum(x is not None for x in [judge, reward_funcs]) if reward_configs == 0: raise ValueError("One of `judge` or `reward_funcs` must be provided.") elif reward_configs > 1: if judge is not None: logger.warning( "Both `judge` and `reward_funcs` are provided. Using `judge` and ignoring `reward_funcs`.", UserWarning, ) reward_funcs = None self.judge = judge # Handle reward_funcs if reward_funcs is not None: if not isinstance(reward_funcs, list): reward_funcs = [reward_funcs] self.reward_func_names = [] # Process reward functions (convert strings to models, collect names) model_init_kwargs = args.model_init_kwargs or {} for i, reward_func in enumerate(reward_funcs): if isinstance(reward_func, str): # Load model from string path reward_funcs[i] = AutoModelForSequenceClassification.from_pretrained( reward_func, num_labels=1, **model_init_kwargs ) if isinstance(reward_funcs[i], nn.Module): self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1]) else: self.reward_func_names.append(reward_funcs[i].__name__) self.reward_funcs = reward_funcs # Handle reward processing classes for reward_funcs if reward_processing_classes is None: reward_processing_classes = [None] * len(reward_funcs) elif not isinstance(reward_processing_classes, list): reward_processing_classes = [reward_processing_classes] else: if len(reward_processing_classes) != len(reward_funcs): raise ValueError( "The number of reward processing classes must match the number of reward functions." ) self.reward_processing_classes = [] for reward_processing_class_i, reward_func in zip(reward_processing_classes, reward_funcs, strict=True): if isinstance(reward_func, PreTrainedModel): if reward_processing_class_i is None: reward_processing_class_i = AutoTokenizer.from_pretrained(reward_func.config._name_or_path) if reward_processing_class_i.pad_token_id is None: reward_processing_class_i.pad_token = reward_processing_class_i.eos_token # Set pad token ID on reward model config reward_func.config.pad_token_id = reward_processing_class_i.pad_token_id self.reward_processing_classes.append(reward_processing_class_i) else: self.reward_funcs = None self.reward_func_names = [] self.reward_processing_classes = [] # Handle reward_weights if reward_funcs is not None: if args.reward_weights is not None: if len(args.reward_weights) != len(self.reward_funcs): raise ValueError( f"Number of reward weights ({len(args.reward_weights)}) must match number of reward " f"functions ({len(self.reward_funcs)})" ) self.reward_weights = torch.tensor(args.reward_weights, dtype=torch.float32) else: self.reward_weights = torch.ones(len(self.reward_funcs), dtype=torch.float32) else: self.reward_weights = None if args.missing_eos_penalty is not None and reward_funcs is None and judge is None: raise ValueError("`missing_eos_penalty` is only supported when `reward_funcs` is provided.") if args is None: raise ValueError("`args` must be provided.") # Check that the processing_class is provided if processing_class is None: raise ValueError("`processing_class` must be provided.") model_init_kwargs = args.model_init_kwargs or {} if isinstance(model, str): model_id = model # Handle dtype in model_init_kwargs dtype = model_init_kwargs.get("dtype", "auto") if isinstance(dtype, torch.dtype) or dtype == "auto" or dtype is None: pass elif isinstance(dtype, str): dtype = getattr(torch, dtype) model_init_kwargs["dtype"] = dtype else: raise ValueError( "Invalid `dtype` passed to `OnlineDPOConfig`. Expected either 'auto' or a string " f"representing a `torch.dtype` (e.g., 'float32'), but got {dtype}." ) model_init_kwargs["device_map"] = model_init_kwargs.get("device_map", "auto") model = AutoModelForCausalLM.from_pretrained(model_id, **model_init_kwargs) else: if args.model_init_kwargs is not None: raise ValueError( "You passed `model_init_kwargs` to the `OnlineDPOConfig`, but your model is already instantiated. " "This argument can only be used when the `model` argument is a string." ) self.is_encoder_decoder = model.config.is_encoder_decoder self.is_vision_model = model.config.model_type in MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES.keys() if peft_config is not None or (is_peft_available() and isinstance(model, PeftModel)): model = prepare_peft_model(model, peft_config, args) # Enable gradient checkpointing if requested if args.gradient_checkpointing: model = self._enable_gradient_checkpointing(model, args) # Disable dropout in the model and reference model if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) # Handle the ref_model # Usually, the user wants the ref model to be the initial version of the model. When using PEFT, it's easy to # get the ref model, as it's just the model with a disabled adapter. When not using PEFT, we need to create # the ref model from the model by copying it and disable the gradients and set it in evaluation mode. if ref_model is None: # No ref model provided, the most common case if peft_config is None: self.ref_model = create_reference_model(model) # copy, disable gradients, set eval mode else: self.ref_model = None # we don't need a ref model here, we can just disable the adapter. else: # rare case, the user provided a ref model self.ref_model = ref_model self.ref_model.eval() # Disable the gradient and set the reward model in eval mode if reward_funcs is not None: for reward_func in reward_funcs: if isinstance(reward_func, PreTrainedModel): reward_func.eval() self.max_length = args.max_length self.stats = { "objective/kl": [], "objective/entropy": [], "objective/non_score_reward": [], "rewards/chosen": [], "rewards/rejected": [], "rewards/accuracies": [], "rewards/margins": [], "logps/chosen": [], "logps/rejected": [], "val/contain_eos_token": [], "beta": [], } if self.reward_funcs is not None: self.stats["objective/rlhf_reward"] = [] self.stats["objective/scores_margin"] = [] self.stats["objective/scores"] = [] # Store generation parameters for later use self.use_vllm = args.use_vllm self.num_generations = 2 # Generate 2 completions per prompt for Online DPO self.temperature = args.temperature self.top_p = args.top_p self.top_k = args.top_k self.min_p = args.min_p self.repetition_penalty = args.repetition_penalty self.use_transformers_paged = args.use_transformers_paged self.vllm_mode = args.vllm_mode if args.use_vllm else None self.vllm_gpu_memory_utilization = args.vllm_gpu_memory_utilization self.vllm_tensor_parallel_size = args.vllm_tensor_parallel_size self.vllm_model_impl = args.vllm_model_impl # Handle pad token for processors or tokenizers if isinstance(processing_class, ProcessorMixin): tokenizer = processing_class.tokenizer elif isinstance(processing_class, PreTrainedTokenizerBase): tokenizer = processing_class else: raise TypeError("The `processing_class` must be either a `PreTrainedTokenizerBase` or a `ProcessorMixin`") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token self.pad_token = tokenizer.pad_token self.pad_token_id = tokenizer.pad_token_id self.eos_token_id = tokenizer.eos_token_id # Vision tokens for VLM support self.image_token_id = getattr(processing_class, "image_token_id", None) self.vision_start_token_id = getattr(processing_class, "vision_start_token_id", None) self.vision_end_token_id = getattr(processing_class, "vision_end_token_id", None) # Get the image token string for token collapsing self.image_token = None if self.image_token_id is not None: self.image_token = tokenizer.decode([self.image_token_id]) # Define the collator if not provided if data_collator is None: data_collator = DPODataCollatorWithPadding(pad_token_id=self.pad_token_id) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) self._beta = args.beta # Set up generation configuration and vLLM after super().__init__ if self.use_vllm: if not is_vllm_available(): raise ImportError( "vLLM is not available and `use_vllm` is set to True. Please install vLLM with " "`pip install trl[vllm]` to use it." ) if self.vllm_mode == "server": if self.accelerator.is_main_process: if args.vllm_server_base_url is not None: base_url = args.vllm_server_base_url else: base_url = f"http://{args.vllm_server_host}:{args.vllm_server_port}" self.vllm_client = VLLMClient( base_url=base_url, group_port=args.vllm_group_port, connection_timeout=args.vllm_server_timeout ) # Determine device type (supports cuda, xpu, etc.) accelerator_type = torch.accelerator.current_accelerator().type current_device = getattr(torch, accelerator_type).current_device() self.vllm_client.init_communicator(device=current_device) else: self.vllm_client = None elif self.vllm_mode == "colocate": # vLLM dynamically adjusts the size of the key-value cache based on available GPU memory at instantiation. # A larger cache size improves speed, so we would expect gpu_memory_utilization=1. # However, at this stage, the optimizer's weights are not yet loaded onto the GPU; they will be loaded # after the first optimizer step and remain in GPU memory throughout training. So we must reserve enough # space for them. # Configure vLLM parameters vllm_quantization = None if is_bitsandbytes_available(): for _, module in model.named_modules(): if isinstance(module, bnb.nn.Linear4bit): vllm_quantization = "bitsandbytes" break elif isinstance(module, bnb.nn.Linear8bitLt): raise ValueError("vLLM does not support in-flight 8-bit quantization.") vllm_kwargs = { "model": model.name_or_path, "tensor_parallel_size": self.vllm_tensor_parallel_size, "gpu_memory_utilization": self.vllm_gpu_memory_utilization, "model_impl": self.vllm_model_impl, "max_num_seqs": self.args.per_device_train_batch_size * self.vllm_tensor_parallel_size, "max_model_len": args.max_length + args.max_new_tokens, # max_length includes prompt + completion "distributed_executor_backend": "external_launcher", # Feed identical seed for tp groups to ensure sampling results are the same across workers "seed": self.accelerator.process_index // self.vllm_tensor_parallel_size, # Latest vLLM v1 memory profiler is misled by the high default value (i.e., 32768) "max_num_batched_tokens": 4096, "enable_sleep_mode": self.args.vllm_enable_sleep_mode, "quantization": vllm_quantization, } # vLLM requires the environment variables to be set for distributed training. os.environ["RANK"] = str(self.accelerator.process_index) os.environ["LOCAL_RANK"] = str(self.accelerator.local_process_index) os.environ["WORLD_SIZE"] = str(self.accelerator.num_processes) # Ensure distributed rendezvous variables are set without colliding across concurrent runs ensure_master_addr_port() self.llm = LLM(**vllm_kwargs) if self.args.vllm_enable_sleep_mode: self.llm.sleep(level=2) else: raise ValueError(f"vllm_mode must be either 'server' or 'colocate', got '{self.vllm_mode}'.") # vLLM specific sampling arguments self.structured_outputs_regex = args.vllm_structured_outputs_regex self._last_loaded_step = -1 # tag to avoid useless loading during grad accumulation # Set up vLLM generation config generation_params = { "n": 2, # 2 generations per prompt for Online DPO "repetition_penalty": self.repetition_penalty, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k, "min_p": 0.0 if self.min_p is None else self.min_p, "max_tokens": args.max_new_tokens, "detokenize": False, # to avoid vllm to decode (we don't need it) } if args.generation_kwargs is not None: generation_params.update(args.generation_kwargs) if self.structured_outputs_regex is not None: if generation_params.get("structured_outputs") is not None: logger.warning( "Both `vllm_structured_outputs_regex` and `generation_kwargs['structured_outputs']` are set; " "`vllm_structured_outputs_regex` takes precedence." ) generation_params["structured_outputs"] = StructuredOutputsParams(regex=self.structured_outputs_regex) elif isinstance(generation_params.get("structured_outputs"), dict): structured_outputs_dict = generation_params.get("structured_outputs") generation_params["structured_outputs"] = StructuredOutputsParams(**structured_outputs_dict) self.generation_config = SamplingParams(**generation_params) # When using vLLM, the main process is responsible for loading the model weights. This can cause process # desynchronization and seems to lead to DeepSpeed hanging during initialization. To prevent this, we # synchronize all processes after vLLM has been fully initialized. self.accelerator.wait_for_everyone() else: # Set up transformers generation config generation_kwargs = { "max_new_tokens": args.max_new_tokens, "do_sample": True, "pad_token_id": self.pad_token_id, "bos_token_id": tokenizer.bos_token_id, "eos_token_id": self.eos_token_id, "temperature": self.temperature, "top_k": self.top_k, "top_p": self.top_p, "repetition_penalty": self.repetition_penalty, "use_cache": True if not self.args.gradient_checkpointing else False, } # Add min_p if supported if self.min_p is not None: generation_kwargs["min_p"] = self.min_p if args.generation_kwargs is not None: generation_kwargs.update(args.generation_kwargs) # Remove None values generation_kwargs = {k: v for k, v in generation_kwargs.items() if v is not None} self.generation_config = GenerationConfig(**generation_kwargs) # Keep training-specific generation kwargs to overwrite model's original generation config self.generation_kwargs = generation_kwargs if self.ref_model is not None: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) elif self.is_fsdp_enabled: self.ref_model = prepare_fsdp(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) if self.reward_funcs is not None: for i, reward_func in enumerate(self.reward_funcs): if isinstance(reward_func, PreTrainedModel): if self.is_deepspeed_enabled: self.reward_funcs[i] = prepare_deepspeed(reward_func, self.accelerator) else: # set device placement to True to make `prepare_model` move `reward_func` to device when using fsdp self.reward_funcs[i] = self.accelerator.prepare_model( reward_func, evaluation_mode=True, device_placement=True ) @property def beta(self): if isinstance(self._beta, list): epoch = self.state.epoch return self._beta[epoch] if epoch < len(self._beta) else self._beta[-1] else: return self._beta @staticmethod def tokenize_row(feature, is_encoder_decoder: bool, tokenizer: PreTrainedTokenizerBase) -> dict[str, Any]: """Tokenize a single row from a DPO specific dataset.""" if not is_encoder_decoder: batch = tokenizer(feature["prompt"], add_special_tokens=False) # Add BOS token to head of prompt. Avoid adding if it's already there if tokenizer.bos_token_id is not None: prompt_len_input_ids = len(batch["input_ids"]) if prompt_len_input_ids == 0 or tokenizer.bos_token_id != batch["input_ids"][0]: batch["input_ids"] = [tokenizer.bos_token_id] + batch["input_ids"] batch["attention_mask"] = [1] + batch["attention_mask"] else: batch = tokenizer(feature["prompt"], add_special_tokens=True) batch = {f"prompt_{key}": value for key, value in batch.items()} return batch def _enable_gradient_checkpointing(self, model: PreTrainedModel, args: OnlineDPOConfig) -> PreTrainedModel: """Enables gradient checkpointing for the model.""" # Ensure use_cache is disabled model.config.use_cache = False # Enable gradient checkpointing on the base model for PEFT if is_peft_model(model): model.base_model.gradient_checkpointing_enable() # Enable gradient checkpointing for non-PEFT models else: model.gradient_checkpointing_enable() model.enable_input_require_grads() return model def _generate_vllm(self, prompts, images=None): eos_token_id = self.eos_token_id pad_token_id = self.pad_token_id # Generate completion_ids and prompt_ids based on mode if self.vllm_mode == "server": completion_ids, prompt_ids = self._generate_vllm_server(prompts, images) elif self.vllm_mode == "colocate": completion_ids, prompt_ids = self._generate_vllm_colocate(prompts, images) # Shared padding, masking, and tensor conversion logic max_prompt_length = max(len(ids) for ids in prompt_ids) prompt_mask = [[0] * (max_prompt_length - len(ids)) + [1] * len(ids) for ids in prompt_ids] prompt_ids = [[pad_token_id] * (max_prompt_length - len(ids)) + ids for ids in prompt_ids] max_tokens = self.generation_config.max_tokens completion_mask = [[1] * len(ids) + [0] * (max_tokens - len(ids)) for ids in completion_ids] completion_ids = [ ids + [eos_token_id] if ids[-1] != eos_token_id and len(ids) < max_tokens else ids for ids in completion_ids ] completion_ids = [ids + [pad_token_id] * (max_tokens - len(ids)) for ids in completion_ids] # Convert to tensors prompt_ids = torch.tensor(prompt_ids, device=self.accelerator.device) prompt_mask = torch.tensor(prompt_mask, device=self.accelerator.device) completion_ids = torch.tensor(completion_ids, device=self.accelerator.device) completion_mask = torch.tensor(completion_mask, device=self.accelerator.device) return prompt_ids, prompt_mask, completion_ids, completion_mask def _generate_vllm_server(self, prompts, images=None): """Generate completions using vLLM server mode""" has_images = images is not None # Update vLLM server weights if needed if hasattr(self, "_last_loaded_step") and self.state.global_step != self._last_loaded_step: self._move_model_to_vllm() self._last_loaded_step = self.state.global_step elif not hasattr(self, "_last_loaded_step"): self._move_model_to_vllm() self._last_loaded_step = self.state.global_step # Apply chat template if conversational if is_conversational({"prompt": prompts[0]}): prompts_text = [apply_chat_template({"prompt": p}, self.processing_class)["prompt"] for p in prompts] else: prompts_text = prompts # Gather all prompts to main process all_prompts = gather_object(prompts_text) if has_images: all_images = gather_object(images) if self.accelerator.is_main_process: # Since 'prompts' contains 'num_generations' duplicates, we first take unique prompts, and generate # num_generations outputs for each one. This is faster than generating outputs for each duplicate # prompt individually. ordered_set_of_prompts = all_prompts[:: self.num_generations] if has_images: ordered_set_of_images = [ [img] if img is not None else None for img in all_images[:: self.num_generations] ] else: ordered_set_of_images = None completion_ids = self.vllm_client.generate( prompts=ordered_set_of_prompts, images=ordered_set_of_images, n=self.num_generations, repetition_penalty=self.repetition_penalty, temperature=self.temperature, top_p=self.top_p, top_k=-1 if self.top_k is None else self.top_k, min_p=0.0 if self.min_p is None else self.min_p, max_tokens=self.generation_config.max_tokens, structured_outputs_regex=self.structured_outputs_regex if hasattr(self, "structured_outputs_regex") else None, generation_kwargs=self.args.generation_kwargs, )["completion_ids"] # Flatten: each prompt generates 2 completions completion_ids = [[comp_id] for prompt_completions in completion_ids for comp_id in prompt_completions] else: completion_ids = [None] * (len(all_prompts) * 2) # Broadcast completions to all processes completion_ids = broadcast_object_list(completion_ids, from_process=0) # Each process takes its slice process_slice = slice( self.accelerator.process_index * len(prompts) * 2, (self.accelerator.process_index + 1) * len(prompts) * 2, ) completion_ids = completion_ids[process_slice] # Create prompt_ids by tokenizing locally prompt_inputs = self.processing_class( text=prompts_text, return_tensors="pt", padding=True, padding_side="left", add_special_tokens=False, ) prompt_ids = [] for prompt_tokens in prompt_inputs["input_ids"]: prompt_ids.extend([prompt_tokens.tolist(), prompt_tokens.tolist()]) # 2 copies for 2 completions return completion_ids, prompt_ids def _generate_vllm_colocate(self, prompts, images=None): """Generate completions using vLLM colocate mode""" if self.args.vllm_enable_sleep_mode: # wake up colocated vLLM instances if needed torch.cuda.empty_cache() # required to avoid OOM in some cases self.llm.wake_up(tags=["weights"]) # Update model weights if needed - only after gradient accumulation completes if self.state.global_step != self._last_loaded_step: self._move_model_to_vllm() self._last_loaded_step = self.state.global_step # Apply chat template if conversational if is_conversational({"prompt": prompts[0]}): prompts_text = [apply_chat_template({"prompt": p}, self.processing_class)["prompt"] for p in prompts] else: prompts_text = prompts # Prepare vLLM inputs with images if available if images is not None: vllm_inputs = [] for prompt, image in zip(prompts_text, images, strict=True): if image is not None: vllm_inputs.append({"prompt": prompt, "multi_modal_data": {"image": image}}) else: vllm_inputs.append(prompt) else: vllm_inputs = prompts_text if self.args.vllm_enable_sleep_mode: self.llm.wake_up(tags=["kv_cache"]) outputs = self.llm.generate(vllm_inputs, self.generation_config, use_tqdm=False) completion_ids = [list(output.outputs[i].token_ids) for i in range(2) for output in outputs] prompt_ids = [list(output.prompt_token_ids) for _ in range(2) for output in outputs] if self.args.vllm_enable_sleep_mode: self.llm.sleep(level=2) return completion_ids, prompt_ids def _sync_fsdp2_params_to_vllm(self, module: nn.Module): # For FSDP2, module.state_dict() already covers all parameters, so no need for recursion for name, param in module.state_dict().items(): # When using PEFT, we need to recover the original parameter name name = name.removeprefix("base_model.model.").replace(".base_layer", "") # Skip PEFT layers: they don’t exist in vLLM, and they are merged already. if is_peft_model(module) and module.prefix in name: continue # When module to save, remove its prefix and discard the original module if "original_module" in name: continue name = self._fix_param_name_to_vllm(name, extra_prefixes=["modules_to_save.default."]) if param.is_cpu: param = param.to(torch.device("cuda")) param = param.full_tensor() if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(name, param) elif self.vllm_mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param)]) def _move_model_to_vllm(self): # For DeepSpeed ZeRO-3 and FSDP, we need to gather all parameters before operations deepspeed_plugin = self.accelerator.state.deepspeed_plugin zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 if zero_stage_3: import deepspeed gather_if_zero3 = deepspeed.zero.GatheredParameters else: gather_if_zero3 = nullcontext if is_peft_model(self.model): # With PEFT and FSDP/DeepSpeed ZeRO Stage 3, we must gather the full model at once before merging, as # merging adapters in a sharded manner is not supported. # TODO: does this work with FSDP? with gather_if_zero3(list(self.model.parameters())): self.model.merge_adapter() # Update vLLM weights while parameters are gathered if self.is_fsdp_enabled: # note if using FSDP, gather_if_zero3 is nullcontext # Update vLLM weights while parameters are gathered # For PEFT with FSDP we need to use the memory efficient post-order traversal fsdp_plugin = getattr(self.accelerator.state, "fsdp_plugin", None) fsdp_version = getattr(fsdp_plugin, "fsdp_version", 1) if fsdp_plugin else 1 if fsdp_version == 1: self._sync_fsdp1_params_to_vllm( self.model ) # use memory-efficient post-order traversal for FSDP elif fsdp_version == 2: self._sync_fsdp2_params_to_vllm(self.model) else: # DeepSpeed ZeRO-3 with PEFT for name, param in self.model.named_parameters(): # When using PEFT, we need to recover the original parameter name name = name.removeprefix("base_model.model.").replace(".base_layer", "") # Skip PEFT layers: they don’t exist in vLLM, and they are merged already. if self.model.prefix in name: continue # When module to save, remove its prefix and discard the original module if "original_module" in name: continue name = self._fix_param_name_to_vllm(name, extra_prefixes=["modules_to_save.default."]) if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.vllm_mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Unmerge adapters while parameters are still gathered self.model.unmerge_adapter() # Parameters will automatically be repartitioned when exiting the context else: # For non-PEFT models, simply gather (if needed) and update each parameter individually. if self.is_fsdp_enabled: fsdp_plugin = getattr(self.accelerator.state, "fsdp_plugin", None) fsdp_version = getattr(fsdp_plugin, "fsdp_version", 1) if fsdp_plugin else 1 if fsdp_version == 1: self._sync_fsdp1_params_to_vllm(self.model) # use memory-efficient post-order traversal for FSDP elif fsdp_version == 2: self._sync_fsdp2_params_to_vllm(self.model) else: for name, param in self.model.named_parameters(): name = self._fix_param_name_to_vllm(name) with gather_if_zero3([param]): if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.vllm_mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Reset cache on vLLM if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.reset_prefix_cache() elif self.vllm_mode == "colocate": self.llm.reset_prefix_cache() def _sync_fsdp1_params_to_vllm(self, module: nn.Module, prefix: str = "", visited=None): """Memory-efficient post-order traversal of FSDP modules to extract full parameters and sync with vLLM.""" # For FSDP1, we need to recurse into children and also use summon_full_params if visited is None: visited = set() for child_name, child_module in module.named_children(): child_prefix = f"{prefix}.{child_name}" if prefix else child_name self._sync_fsdp1_params_to_vllm( child_module, prefix=child_prefix, visited=visited ) # recurse into the child if isinstance(module, FSDP): with FSDP.summon_full_params(module, recurse=False, writeback=False): for param_name, param in module.named_parameters(): full_name = f"{prefix}.{param_name}" if prefix else param_name full_name = self._fix_param_name_to_vllm(full_name, extra_prefixes=["_fsdp_wrapped_module."]) if full_name in visited: continue # skip FSDP subtrees already traversed visited.add(full_name) if self.vllm_mode == "server" and self.accelerator.is_main_process: self.vllm_client.update_named_param(full_name, param.data) elif self.vllm_mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(full_name, param.data)]) def _fix_param_name_to_vllm(self, name, extra_prefixes: list[str] | None = None): """Clean parameter names for vLLM compatibility""" extra_prefixes = extra_prefixes or [] prefixes = ["_checkpoint_wrapped_module."] + extra_prefixes for prefix in prefixes: name = name.replace(prefix, "") return name def process_vision_row( self, features: dict[str, list | torch.Tensor], processing_class=None ) -> dict[str, list[int]]: """ Process a vision row for VLM models (adapted from DPO trainer) """ processor = processing_class or self.processing_class processed_features = processor(images=[features["image"]], text=features["prompt"], add_special_tokens=False) prompt_input_ids = processed_features["input_ids"][0] # Create the output dict with required fields output = { "prompt_input_ids": prompt_input_ids, "prompt_attention_mask": processed_features["attention_mask"][0], } # Add vision-specific fields if "pixel_values" in processed_features: output["pixel_values"] = processed_features["pixel_values"][0] if "pixel_attention_mask" in processed_features: output["pixel_attention_mask"] = processed_features["pixel_attention_mask"][0] if "image_sizes" in processed_features: output["image_sizes"] = processed_features["image_sizes"][0] return output def _generate(self, model, prompts, images=None): """Generate completions using the model""" device = next(model.parameters()).device eos_token_id = self.eos_token_id pad_token_id = self.pad_token_id # Apply chat template and tokenize the input inputs = [{"prompt": prompt} for prompt in prompts] # Add images if provided (VLM support) if images is not None: for i, image in enumerate(images): inputs[i]["image"] = image # Apply chat template to get text prompts prompts_text = [maybe_apply_chat_template(x, self.processing_class)["prompt"] for x in inputs] # Handle image token collapsing/removal # The chat template sometimes inserts a single image token into the prompt text. However, when this text is # later tokenized, the single image token string is expanded into multiple image token IDs, depending on the # image size. We need to handle this properly. if self.image_token is not None and images is not None: escaped_img_token = re.escape(self.image_token) # Search for the image token in the chat template if hasattr(self.processing_class, "chat_template") and self.processing_class.chat_template: if re.search(escaped_img_token, self.processing_class.chat_template): # Collapse repeated image tokens back into a single token prompts_text = [ re.sub(rf"({escaped_img_token})+", self.image_token, text) for text in prompts_text ] else: # If the chat template doesn't use the image token, remove all instances if self.vision_end_token_id is not None: escaped_eoi_token = re.escape( self.processing_class.tokenizer.decode([self.vision_end_token_id]) ) prompts_text = [ re.sub(rf"({escaped_img_token})+{escaped_eoi_token}", "", text) for text in prompts_text ] else: # If vision_end_token_id is None, just remove the image tokens prompts_text = [re.sub(rf"({escaped_img_token})+", "", text) for text in prompts_text] # Prepare kwargs for processing class kwargs = {} if images is not None: kwargs = {"images": [[img] for img in images]} # Process inputs using the processing class (handles both VLM and LLM) prompt_inputs = self.processing_class( text=prompts_text, return_tensors="pt", padding=True, padding_side="left", add_special_tokens=False, **kwargs, ) prompt_inputs = {k: v.to(device) for k, v in prompt_inputs.items()} # Convert vision inputs to model's dtype for proper computation if "pixel_values" in prompt_inputs: # Handle DataParallel wrapped models model_dtype = getattr(model, "dtype", None) if model_dtype is None and hasattr(model, "module"): model_dtype = model.module.dtype if model_dtype is not None: prompt_inputs["pixel_values"] = prompt_inputs["pixel_values"].to(model_dtype) # Sample 2 completions per prompt of size `max_new_tokens` from the model prompt_ids = prompt_inputs["input_ids"].repeat(2, 1) prompt_mask = prompt_inputs["attention_mask"].repeat(2, 1) # Prepare vision inputs if available vision_generation_kwargs = {} if self.is_vision_model and images is not None: if "pixel_values" in prompt_inputs: vision_generation_kwargs["pixel_values"] = prompt_inputs["pixel_values"].repeat(2, 1, 1, 1) if "pixel_attention_mask" in prompt_inputs: vision_generation_kwargs["pixel_attention_mask"] = prompt_inputs["pixel_attention_mask"].repeat(2, 1) if "image_sizes" in prompt_inputs: vision_generation_kwargs["image_sizes"] = prompt_inputs["image_sizes"].repeat(2, 1) if "image_grid_thw" in prompt_inputs: vision_generation_kwargs["image_grid_thw"] = prompt_inputs["image_grid_thw"].repeat(2, 1) if self.use_transformers_paged: previous_attn = self.model_wrapped.config._attn_implementation if Version(transformers.__version__).release >= Version("5.0.0").release: new_attn = "paged|flash_attention_2" if is_flash_attn_2_available() else "paged|sdpa" else: new_attn = "paged_attention" if is_flash_attn_2_available() else "sdpa_paged" self.model_wrapped.config._attn_implementation = new_attn with ( profiling_context(self, "transformers.generate_batch"), unwrap_model_for_generation( model, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): # Cast to the appropriate dtype based on training configuration if self.args.bf16: unwrapped_model.to(torch.bfloat16) elif self.args.fp16: unwrapped_model.to(torch.float16) with torch.inference_mode(): all_outputs = unwrapped_model.generate_batch( prompt_ids.tolist(), generation_config=self.generation_config, progress_bar=False, ) unwrapped_model.train() # restore training mode, as generate_batch forces eval mode completion_ids = [output.generated_tokens for output in all_outputs.values()] completion_ids = [torch.tensor(ids, device=device) for ids in completion_ids] completion_ids = pad(completion_ids, padding_value=self.pad_token_id, padding_side="right") prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # Restore the original attention implementation, training mode self.model_wrapped.config._attn_implementation = previous_attn # Extract completion_ids and create completion_mask prompt_length = prompt_ids.size(1) completion_ids = prompt_completion_ids[:, prompt_length:] completion_ids, completion_mask = truncate_right(completion_ids, eos_token_id, pad_token_id) return prompt_ids, prompt_mask, completion_ids, completion_mask else: # Regular generation path with ( profiling_context(self, "transformers.generate"), unwrap_model_for_generation( model, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): # Setup cache implementation if specified if self.args.cache_implementation is not None: unwrapped_model.generation_config.cache_implementation = self.args.cache_implementation # Standard generation output = unwrapped_model.generate( input_ids=prompt_ids, attention_mask=prompt_mask, generation_config=self.generation_config, **vision_generation_kwargs, ) completion_ids = output[:, prompt_ids.size(1) :] completion_ids, completion_mask = truncate_right(completion_ids, eos_token_id, pad_token_id) return prompt_ids, prompt_mask, completion_ids, completion_mask def _calculate_rewards_from_functions(self, prompts, completions, completion_ids_list, **reward_kwargs): """ Calculate rewards using reward functions """ device = self.accelerator.device rewards_per_func = torch.zeros(len(prompts), len(self.reward_funcs), device=device) # Add trainer state to reward kwargs for dynamic reward shaping reward_kwargs["trainer_state"] = self.state for i, (reward_func, reward_processing_class) in enumerate( zip(self.reward_funcs, self.reward_processing_classes, strict=True) ): if isinstance(reward_func, nn.Module): # Model-based reward function # Handle conversational vs text input if is_conversational({"prompt": prompts[0]}): messages = [{"messages": p + c} for p, c in zip(prompts, completions, strict=True)] texts = [apply_chat_template(x, reward_processing_class)["text"] for x in messages] else: texts = [p + c for p, c in zip(prompts, completions, strict=True)] # Tokenize and get reward scores reward_inputs = reward_processing_class( text=texts, return_tensors="pt", padding=True, padding_side="right", add_special_tokens=False ) reward_inputs = {k: v.to(device) for k, v in reward_inputs.items()} with torch.inference_mode(): rewards_per_func[:, i] = reward_func(**reward_inputs).logits[:, 0] # Shape (B*G,) else: # Custom reward function output_reward_func = reward_func( prompts=prompts, completions=completions, completion_ids=completion_ids_list, **reward_kwargs ) # Convert None values to NaN output_reward_func = [reward if reward is not None else torch.nan for reward in output_reward_func] rewards_per_func[:, i] = torch.tensor(output_reward_func, dtype=torch.float32, device=device) # Weight and sum across all reward functions if self.reward_weights is not None: total_rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) else: total_rewards = rewards_per_func.nansum(dim=1) return total_rewards def _forward(self, model, prompt_ids, prompt_mask, completion_ids, completion_mask, vision_inputs=None): # Get the number of tokens to truncate from prompt num_tokens_to_truncate = max(prompt_ids.size(1) + completion_ids.size(1) - self.max_length, 0) # Truncate left to avoid oom prompt_ids = prompt_ids[:, num_tokens_to_truncate:] prompt_mask = prompt_mask[:, num_tokens_to_truncate:] # Concat the prompt and completion prompt_completion_ids = torch.cat((prompt_ids, completion_ids), dim=1) prompt_completion_mask = torch.cat((prompt_mask, completion_mask), dim=1) # Prepare model kwargs with vision inputs if available model_kwargs = {"attention_mask": prompt_completion_mask} if vision_inputs is not None: if "pixel_values" in vision_inputs: model_kwargs["pixel_values"] = vision_inputs["pixel_values"] if "pixel_attention_mask" in vision_inputs: model_kwargs["pixel_attention_mask"] = vision_inputs["pixel_attention_mask"] if "image_sizes" in vision_inputs: model_kwargs["image_sizes"] = vision_inputs["image_sizes"] if "image_grid_thw" in vision_inputs: model_kwargs["image_grid_thw"] = vision_inputs["image_grid_thw"] # Get the logprobs of the completions from the model output = model(prompt_completion_ids, **model_kwargs) # There is 1 offset, because the model predicts the next token prompt_len = prompt_ids.size(1) start_idx = prompt_len - 1 if prompt_len > 0 else 0 # Only slice off the last logit when we have a prompt, otherwise we need all logits end_idx = -1 if prompt_len > 0 else None logits = output.logits[:, start_idx:end_idx] # Take the completion tokens logprob logprobs = torch.take_along_dim(logits.log_softmax(dim=-1), completion_ids.unsqueeze(-1), dim=2).squeeze(-1) return logprobs def training_step( self, model: nn.Module, inputs: dict[str, torch.Tensor | Any], num_items_in_batch: int | None = None ) -> torch.Tensor: model.train() prompts = inputs["prompt"] batch_size = len(prompts) # Handle images for VLM support has_images = "image" in inputs images = None if has_images: images = inputs["image"] # Convert conversational prompts to include image tokens for prompt in prompts: if isinstance(prompt, list): for message in prompt: if not isinstance(message, dict): continue content = message.get("content") role = message.get("role") if isinstance(content, str): if role == "user": message["content"] = [{"type": "image"}, {"type": "text", "text": content}] elif role == "system": message["content"] = [{"type": "text", "text": content}] if self.args.use_vllm: prompt_ids, prompt_mask, completion_ids, completion_mask = self._generate_vllm(prompts, images) else: prompt_ids, prompt_mask, completion_ids, completion_mask = self._generate(model, prompts, images) contain_eos_token = torch.any(completion_ids == self.eos_token_id, dim=-1) # Extract vision inputs if available for VLM support vision_inputs = None if has_images and self.is_vision_model and not self.args.use_vllm: # For vision models with transformers generation, we need to prepare vision inputs # Process the images to get vision inputs that can be passed through the forward pass vision_inputs = {} kwargs = {"images": [[img] for img in images]} processed = self.processing_class( text=[""] * len(images), # Dummy text for vision processing return_tensors="pt", **kwargs, ) # Handle DataParallel wrapped models model_device = getattr(model, "device", None) model_dtype = getattr(model, "dtype", None) if model_device is None and hasattr(model, "module"): model_device = model.module.device model_dtype = model.module.dtype # Move vision tensors to device and convert to model dtype # Need to duplicate for 2 completions per prompt if "pixel_values" in processed: vision_inputs["pixel_values"] = ( processed["pixel_values"].to(model_device, dtype=model_dtype).repeat(2, 1, 1, 1) ) if "pixel_attention_mask" in processed: vision_inputs["pixel_attention_mask"] = processed["pixel_attention_mask"].to(model_device).repeat(2, 1) if "image_sizes" in processed: vision_inputs["image_sizes"] = processed["image_sizes"].to(model_device).repeat(2, 1) if "image_grid_thw" in processed: vision_inputs["image_grid_thw"] = processed["image_grid_thw"].to(model_device).repeat(2, 1) logprobs = self._forward(model, prompt_ids, prompt_mask, completion_ids, completion_mask, vision_inputs) with torch.no_grad(): if self.ref_model is not None: ref_logprobs = self._forward( self.ref_model, prompt_ids, prompt_mask, completion_ids, completion_mask, vision_inputs ) else: # peft case: we just need to disable the adapter with self.model.disable_adapter(): ref_logprobs = self._forward( self.model, prompt_ids, prompt_mask, completion_ids, completion_mask, vision_inputs ) # Decode the completions, and format them if the input is conversational device = logprobs.device completions = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) if is_conversational({"prompt": prompts[0]}): completions = [[{"role": "assistant", "content": completion}] for completion in completions] # Get the reward from reward functions or judge if self.reward_funcs is not None: # First create completion_ids_list for custom reward functions completion_ids_list = [completion_ids[i].tolist() for i in range(completion_ids.shape[0])] # Extract additional fields from inputs for reward functions reward_kwargs = {} keys = [key for key in inputs if key not in ["prompt"]] for key in keys: if isinstance(inputs[key], (list, tuple)): # Repeat input fields to match number of completions (2 per prompt) reward_kwargs[key] = inputs[key] * 2 else: reward_kwargs[key] = inputs[key] # Calculate rewards using reward functions rewards = self._calculate_rewards_from_functions( prompts=2 * prompts, completions=completions, completion_ids_list=completion_ids_list, **reward_kwargs ) # Apply missing EOS penalty if configured if self.args.missing_eos_penalty is not None: rewards[~contain_eos_token] -= self.args.missing_eos_penalty # Split rewards into chosen/rejected pairs first_half, second_half = rewards.split(batch_size) mask = first_half >= second_half elif self.judge is not None: # Once formatted, conversational data may contain special tokens (such as <|im_start|>) that are not # directly understandable by the judge and could alter its judgment. To avoid this and make the judge # independent of the model's chat template, we use the raw conversation data, and apply our own chat # template to it. if is_conversational({"prompt": prompts[0]}): environment = jinja2.Environment() template = environment.from_string(SIMPLE_CHAT_TEMPLATE) prompts = [template.render(messages=prompt) for prompt in prompts] completions = [template.render(messages=completion) for completion in completions] ranks_of_first_completion = self.judge.judge( prompts, list(zip(completions[:batch_size], completions[batch_size:], strict=True)) ) # convert ranks to a True/False mask: # when rank == 0, it means the first completion is the best # when rank == 1, it means the second completion is the best mask = torch.tensor([rank == 0 for rank in ranks_of_first_completion], device=device) batch_range = torch.arange(batch_size, device=device) chosen_indices = batch_range + (~mask * batch_size) rejected_indices = batch_range + (mask * batch_size) # Build tensor so that the first half is the chosen examples and the second half the rejected examples cr_indices = torch.cat((chosen_indices, rejected_indices), dim=0) # cr = chosen and rejected cr_logprobs = logprobs[cr_indices] cr_ref_logprobs = ref_logprobs[cr_indices] # mask out the padding tokens padding_mask = ~completion_mask.bool() cr_padding_mask = padding_mask[cr_indices] cr_logprobs_sum = (cr_logprobs * ~cr_padding_mask).sum(1) cr_ref_logprobs_sum = (cr_ref_logprobs * ~cr_padding_mask).sum(1) # Split the chosen and rejected examples chosen_logprobs_sum, rejected_logprobs_sum = torch.split(cr_logprobs_sum, batch_size) chosen_ref_logprobs_sum, rejected_ref_logprobs_sum = torch.split(cr_ref_logprobs_sum, batch_size) pi_logratios = chosen_logprobs_sum - rejected_logprobs_sum ref_logratios = chosen_ref_logprobs_sum - rejected_ref_logprobs_sum logits = pi_logratios - ref_logratios if self.args.loss_type == "sigmoid": losses = -F.logsigmoid(self.beta * logits) elif self.args.loss_type == "ipo": losses = (logits - 1 / (2 * self.beta)) ** 2 else: raise NotImplementedError(f"invalid loss type {self.args.loss_type}") loss = losses.mean() # Log everything if self.reward_funcs is not None: # When using reward_funcs, we have rewards instead of scores scores_margin = rewards[chosen_indices] - rewards[rejected_indices] self.stats["objective/scores_margin"].append( self.accelerator.gather_for_metrics(scores_margin.mean()).mean().item() ) self.stats["objective/scores"].append(self.accelerator.gather_for_metrics(rewards.mean()).mean().item()) self.stats["val/contain_eos_token"].append(contain_eos_token.float().mean().item()) self.stats["logps/chosen"].append(self.accelerator.gather_for_metrics(chosen_logprobs_sum).mean().item()) self.stats["logps/rejected"].append(self.accelerator.gather_for_metrics(rejected_logprobs_sum).mean().item()) kl = logprobs - ref_logprobs mean_kl = kl.sum(1).mean() self.stats["objective/kl"].append(self.accelerator.gather_for_metrics(mean_kl).mean().item()) non_score_reward = (-self.beta * kl).sum(1) mean_non_score_reward = non_score_reward.mean() self.stats["objective/non_score_reward"].append( self.accelerator.gather_for_metrics(mean_non_score_reward).mean().item() ) if self.reward_funcs is not None: # Calculate RLHF reward by combining rewards with non_score_reward rlhf_reward = rewards + non_score_reward self.stats["objective/rlhf_reward"].append(self.accelerator.gather_for_metrics(rlhf_reward).mean().item()) mean_entropy = -logprobs.sum(1).mean() self.stats["objective/entropy"].append(self.accelerator.gather_for_metrics(mean_entropy).mean().item()) chosen_rewards = self.beta * (chosen_logprobs_sum - chosen_ref_logprobs_sum) gathered_chosen_rewards = self.accelerator.gather_for_metrics(chosen_rewards) self.stats["rewards/chosen"].append(gathered_chosen_rewards.mean().item()) rejected_rewards = self.beta * (rejected_logprobs_sum - rejected_ref_logprobs_sum) gathered_rejected_rewards = self.accelerator.gather_for_metrics(rejected_rewards) self.stats["rewards/rejected"].append(gathered_rejected_rewards.mean().item()) margin = gathered_chosen_rewards - gathered_rejected_rewards self.stats["rewards/margins"].append(margin.mean().item()) accuracy = margin > 0 self.stats["rewards/accuracies"].append(accuracy.float().mean().item()) self.stats["beta"].append(self.beta) if ( self.args.torch_empty_cache_steps is not None and self.state.global_step % self.args.torch_empty_cache_steps == 0 ): empty_cache() kwargs = {} # For LOMO optimizers you need to explicitly use the learning rate if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: kwargs["learning_rate"] = self._get_learning_rate() if self.args.n_gpu > 1: loss = loss.mean() # mean() to average on multi-gpu parallel training self.accelerator.backward(loss, **kwargs) return loss.detach() / self.args.gradient_accumulation_steps # Same as Trainer._maybe_log_save_evaluate but log our metrics def _maybe_log_save_evaluate( self, tr_loss, grad_norm, model, trial, epoch, ignore_keys_for_eval, start_time, learning_rate=None ): if self.control.should_log and self.state.global_step > self._globalstep_last_logged: logs: dict[str, float] = {} # all_gather + mean() to get average loss over all processes if Version(transformers.__version__) >= Version("5.2.0"): tr_loss_scalar = nested_gather(tr_loss, self.args.parallel_mode).mean().item() else: tr_loss_scalar = self._nested_gather(tr_loss).mean().item() # reset tr_loss to zero tr_loss -= tr_loss logs["loss"] = round(tr_loss_scalar / (self.state.global_step - self._globalstep_last_logged), 4) if grad_norm is not None: logs["grad_norm"] = grad_norm.detach().item() if isinstance(grad_norm, torch.Tensor) else grad_norm if learning_rate is not None: logs["learning_rate"] = learning_rate else: logs["learning_rate"] = self._get_learning_rate() # Add our metrics for key, val in self.stats.items(): logs[key] = sum(val) / len(val) self.stats = {key: [] for key in self.stats} # reset stats self._total_loss_scalar += tr_loss_scalar self._globalstep_last_logged = self.state.global_step self.store_flos() self.log(logs, start_time) metrics = None if self.control.should_evaluate: metrics = self._evaluate(trial, ignore_keys_for_eval) is_new_best_metric = self._determine_best_metric(metrics=metrics, trial=trial) if self.args.save_strategy == "best": self.control.should_save = is_new_best_metric if self.control.should_save: self._save_checkpoint(model, trial) self.control = self.callback_handler.on_save(self.args, self.state, self.control) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/openenv/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .utils import generate_rollout_completions __all__ = ["generate_rollout_completions"] ================================================ FILE: trl/experimental/openenv/utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Any import torch from ...data_utils import is_conversational from ...extras.profiling import profiling_context from ...import_utils import is_vllm_available if is_vllm_available(): from vllm import SamplingParams from vllm.sampling_params import StructuredOutputsParams def _build_base_generation_kwargs( trainer, overrides: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build base generation kwargs common to both colocate and server modes.""" generation_kwargs: dict[str, Any] = { "n": 1, "temperature": trainer.temperature, "top_k": trainer.top_k, "min_p": 0.0 if trainer.min_p is None else trainer.min_p, "max_tokens": trainer.max_completion_length, } if trainer.repetition_penalty is not None: generation_kwargs["repetition_penalty"] = trainer.repetition_penalty if trainer.top_p is not None: generation_kwargs["top_p"] = trainer.top_p if trainer.args.generation_kwargs is not None: generation_kwargs.update(trainer.args.generation_kwargs) if overrides is not None: generation_kwargs.update(overrides) generation_kwargs = {key: value for key, value in generation_kwargs.items() if value is not None} if generation_kwargs.get("n", 1) != 1: raise ValueError("generate_rollout_completions expects n=1.") return generation_kwargs def _build_colocate_sampling_params( trainer, overrides: dict[str, Any] | None = None, *, logprobs: bool = True, ) -> "SamplingParams": """Build SamplingParams for colocate mode.""" generation_kwargs = _build_base_generation_kwargs(trainer, overrides) # Add colocate-specific parameters if trainer.vllm_generation.structured_outputs_regex: generation_kwargs["structured_outputs"] = StructuredOutputsParams( regex=trainer.vllm_generation.structured_outputs_regex ) if logprobs: generation_kwargs["logprobs"] = 0 return SamplingParams(**generation_kwargs) def _build_server_generation_kwargs( trainer, overrides: dict[str, Any] | None = None, ) -> dict[str, Any]: """Build generation kwargs for server mode.""" return _build_base_generation_kwargs(trainer, overrides) def generate_rollout_completions( trainer, prompts: list[str], *, generation_overrides: dict[str, Any] | None = None, as_chat: bool | None = None, ) -> list[dict[str, Any]]: """ Generate completions for custom rollouts when vLLM is running in colocate or server mode. Returns one result per prompt, containing prompt and completion token ids along with per-token log probabilities and the generated text. """ if not prompts: return [] if not trainer.use_vllm: raise RuntimeError("Custom rollouts require vLLM to call generate_rollout_completions.") if trainer.vllm_mode == "server": return _generate_rollout_completions_server(trainer, prompts, generation_overrides, as_chat) elif trainer.vllm_mode == "colocate": return _generate_rollout_completions_colocate(trainer, prompts, generation_overrides, as_chat) else: raise ValueError(f"vllm_mode must be 'server' or 'colocate', got '{trainer.vllm_mode}'") def _generate_rollout_completions_server( trainer, prompts: list[str], generation_overrides: dict[str, Any] | None = None, as_chat: bool | None = None, ) -> list[dict[str, Any]]: """Generate completions using vLLM server mode.""" generation_kwargs = _build_server_generation_kwargs(trainer, generation_overrides) if as_chat is None: as_chat = prompts and is_conversational({"prompt": prompts[0]}) with profiling_context(trainer, "vLLM.generate_rollout_server"): if as_chat: # Prompts are raw message dicts; use .chat() so the vLLM server applies the chat template output = trainer.vllm_generation.vllm_client.chat( messages=prompts, **generation_kwargs, chat_template_kwargs=trainer.chat_template_kwargs, tools=trainer.tools or None, chat_template=trainer.chat_template, ) else: output = trainer.vllm_generation.vllm_client.generate(prompts=prompts, **generation_kwargs) # Format results to match colocate output format results: list[dict[str, Any]] = [] for i in range(len(prompts)): results.append( { "prompt_ids": output["prompt_ids"][i], "completion_ids": list(output["completion_ids"][i]), "logprobs": list(output["logprobs"][i]), "text": trainer.processing_class.decode(output["completion_ids"][i], skip_special_tokens=True), } ) return results def _generate_rollout_completions_colocate( trainer, prompts: list[str], generation_overrides: dict[str, Any] | None = None, as_chat: bool | None = None, ) -> list[dict[str, Any]]: """Generate completions using vLLM colocate mode.""" sampling_params = _build_colocate_sampling_params(trainer, generation_overrides) prompts_for_generation = prompts original_size = len(prompts) if trainer.vllm_tensor_parallel_size > 1: gathered_prompts = [None for _ in range(trainer.vllm_tensor_parallel_size)] torch.distributed.all_gather_object(gathered_prompts, prompts, group=trainer.vllm_generation.tp_group) prompts_for_generation = [prompt for group_prompts in gathered_prompts for prompt in group_prompts] if as_chat is None: as_chat = prompts_for_generation and is_conversational({"prompt": prompts_for_generation[0]}) if trainer.args.vllm_enable_sleep_mode: trainer.vllm_generation.llm.wake_up(tags=["kv_cache"]) # Work around for https://github.com/vllm-project/vllm/issues/29341 trainer.vllm_generation.llm.collective_rpc("reload_weights") with profiling_context(trainer, "vLLM.generate_rollout"): if as_chat: vllm_outputs = trainer.vllm_generation.llm.chat( prompts_for_generation, sampling_params=sampling_params, use_tqdm=False ) else: vllm_outputs = trainer.vllm_generation.llm.generate( prompts_for_generation, sampling_params=sampling_params, use_tqdm=False ) results: list[dict[str, Any]] = [] for request in vllm_outputs: if not request.outputs: results.append({"prompt_ids": request.prompt_token_ids, "completion_ids": [], "logprobs": [], "text": ""}) continue sequence = request.outputs[0] logprobs = [next(iter(token_logprob.values())).logprob for token_logprob in sequence.logprobs] results.append( { "prompt_ids": request.prompt_token_ids, "completion_ids": sequence.token_ids, "logprobs": logprobs, "text": sequence.text, } ) if trainer.vllm_tensor_parallel_size > 1: local_rank_in_group = torch.distributed.get_rank(group=trainer.vllm_generation.tp_group) tp_slice = slice(local_rank_in_group * original_size, (local_rank_in_group + 1) * original_size) results = results[tp_slice] if trainer.args.vllm_enable_sleep_mode: trainer.vllm_generation.llm.sleep(level=2) return results ================================================ FILE: trl/experimental/orpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .orpo_config import ORPOConfig from .orpo_trainer import ORPOTrainer __all__ = ["ORPOConfig", "ORPOTrainer"] ================================================ FILE: trl/experimental/orpo/orpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from ...trainer.base_config import _BaseConfig @dataclass class ORPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.orpo.ORPOTrainer`]. This class includes only the parameters that are specific to ORPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the sequences (prompt + completion) in the batch. This argument is required if you want to use the default data collator. max_completion_length (`int`, *optional*): Maximum length of the completion. This argument is required if you want to use the default data collator and your model is an encoder-decoder. beta (`float`, *optional*, defaults to `0.1`): Parameter controlling the relative ratio loss weight in the ORPO loss. In the [paper](https://huggingface.co/papers/2403.07691), it is denoted by λ. In the [code](https://github.com/xfactlab/orpo), it is denoted by `alpha`. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. padding_value (`int`, *optional*): Padding value to use. If `None`, the padding value of the tokenizer is used. truncation_mode (`str`, *optional*, defaults to `"keep_end"`): Truncation mode to use when the prompt is too long. Possible values are `"keep_end"` or `"keep_start"`. This argument is required if you want to use the default data collator. generate_during_eval (`bool`, *optional*, defaults to `False`): If `True`, generates and logs completions from the model to W&B or Comet during evaluation. is_encoder_decoder (`bool`, *optional*): When using the `model_init` argument (callable) to instantiate the model instead of the `model` argument, you need to specify if the model returned by the callable is an encoder-decoder model. model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model from a string. dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) max_length: int | None = field( default=1024, metadata={"help": "Maximum length of the sequences (prompt + completion) in the batch."}, ) max_completion_length: int | None = field( default=None, metadata={ "help": "Maximum length of the completion. This argument is required if you want to use the default data " "collator and your model is an encoder-decoder." }, ) beta: float = field( default=0.1, metadata={ "help": "Parameter controlling the relative ratio loss weight in the ORPO loss. In the paper, it is " "denoted by λ." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model."}, ) padding_value: int | None = field( default=None, metadata={"help": "Padding value to use. If `None`, the padding value of the tokenizer is used."}, ) truncation_mode: str = field( default="keep_end", metadata={ "help": "Truncation mode to use when the prompt is too long.", "choices": ["keep_end", "keep_start"], }, ) generate_during_eval: bool = field( default=False, metadata={"help": "If `True`, generates and logs completions from the model to W&B during evaluation."}, ) is_encoder_decoder: bool | None = field( default=None, metadata={ "help": "When using the `model_init` argument (callable) to instantiate the model instead of the `model` " "argument, you need to specify if the model returned by the callable is an encoder-decoder model." }, ) model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments to pass to `AutoModelForCausalLM.from_pretrained` when instantiating the model " "from a string." }, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) ================================================ FILE: trl/experimental/orpo/orpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import random import textwrap from collections import defaultdict from collections.abc import Callable from contextlib import nullcontext from pathlib import Path from typing import Any, Literal import numpy as np import pandas as pd import torch import torch.nn as nn import torch.nn.functional as F import transformers from accelerate import PartialState, logging from datasets import Dataset from packaging.version import Version from torch import autocast from torch.utils.data import DataLoader from transformers import ( AutoModelForCausalLM, BaseImageProcessor, DataCollator, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, is_comet_available, is_torch_xla_available, is_wandb_available, ) from transformers.trainer_utils import EvalLoopOutput from transformers.utils import is_peft_available, is_torch_fx_proxy from ...data_utils import maybe_apply_chat_template, maybe_extract_prompt from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import disable_dropout_in_model, log_table_to_comet_experiment, selective_log_softmax from ..utils import ( DPODataCollatorWithPadding, add_bos_token_if_needed, add_eos_token_if_needed, pad_to_length, peft_module_casting_to_bf16, ) from .orpo_config import ORPOConfig if is_peft_available(): from peft import PeftModel, get_peft_model, prepare_model_for_kbit_training if is_wandb_available(): import wandb if is_torch_xla_available(): import torch_xla.core.xla_model as xm logger = logging.get_logger(__name__) def log1mexp(x: torch.FloatTensor) -> torch.FloatTensor: """Numerically stable computation of log(1-exp(x)).""" # branch at -ln 2 ~ -0.693 to avoid cancellation t = -0.6931471805599453 return torch.where(x < t, torch.log1p(-torch.exp(x)), torch.log(-torch.expm1(x))) class ORPOTrainer(_BaseTrainer): r""" Initialize ORPOTrainer. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an [`~transformers.AutoModelForSequenceClassification`]. args ([`experimental.orpo.ORPOConfig`]): The ORPO config arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. model_init (`Callable[[], transformers.PreTrainedModel]`): The model initializer to use for training. If None is specified, the default model initializer will be used. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. peft_config (`dict`, defaults to `None`): The PEFT configuration to use for training. If you pass a PEFT configuration, the model will be wrapped in a PEFT model. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. """ _tag_names = ["trl", "orpo"] _name = "ORPO" _paper = { "title": "ORPO: Monolithic Preference Optimization without Reference Model", "id": "2403.07691", # docstyle-ignore "citation": textwrap.dedent("""\ @article{hong2024orpo, title = {{ORPO: Monolithic Preference Optimization without Reference Model}}, author = {Jiwoo Hong and Noah Lee and James Thorne}, year = 2024, eprint = {arXiv:2403.07691} }"""), } def __init__( self, model: PreTrainedModel | nn.Module | str | None = None, args: ORPOConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, model_init: Callable[[], PreTrainedModel] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalLoopOutput], dict] | None = None, ): if train_dataset is None: raise ValueError("`train_dataset` is required") if args.model_init_kwargs is None: model_init_kwargs = {} elif not isinstance(model, str): raise ValueError("You passed model_kwargs to the ORPOTrainer. But your model is already instantiated.") else: model_init_kwargs = args.model_init_kwargs dtype = model_init_kwargs.get("dtype", "auto") if dtype is not None: # Convert to `torch.dtype` if an str is passed if isinstance(dtype, str) and dtype != "auto": dtype = getattr(torch, dtype) if dtype != "auto" and not isinstance(dtype, torch.dtype): raise ValueError( f"Invalid `dtype` passed to the ORPOConfig. Expected a string with either `torch.dtype` or 'auto', but got {dtype}." ) model_init_kwargs["dtype"] = dtype model_init_kwargs["device_map"] = model_init_kwargs.get("device_map", "auto") if isinstance(model, str): model = AutoModelForCausalLM.from_pretrained(model, **model_init_kwargs) # Initialize this variable to False. This helps tracking the case when `peft_module_casting_to_bf16` # has been called in order to properly call autocast if needed. self._peft_has_been_casted_to_bf16 = False if not is_peft_available() and peft_config is not None: raise ValueError( "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it to use the PEFT models" ) elif is_peft_available() and peft_config is not None: if isinstance(model, PeftModel): raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first " "merge and unload the existing adapter, save the resulting base model, and then pass that base " "model along with the new `peft_config` to the trainer." ) if getattr(model, "is_loaded_in_8bit", False) or getattr(model, "is_loaded_in_4bit", False): _support_gc_kwargs = hasattr( args, "gradient_checkpointing_kwargs" ) and "gradient_checkpointing_kwargs" in list( inspect.signature(prepare_model_for_kbit_training).parameters ) prepare_model_kwargs = {"use_gradient_checkpointing": args.gradient_checkpointing} if _support_gc_kwargs: prepare_model_kwargs["gradient_checkpointing_kwargs"] = args.gradient_checkpointing_kwargs model = prepare_model_for_kbit_training(model, **prepare_model_kwargs) elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) # get peft model with the given config model = get_peft_model(model, peft_config) if args.bf16 and getattr(model, "is_loaded_in_4bit", False): peft_module_casting_to_bf16(model) # If args.bf16 we need to explicitly call `generate` with torch amp autocast context manager self._peft_has_been_casted_to_bf16 = True # For models that use gradient_checkpointing, we need to attach a hook that enables input # to explicitly have `requires_grad=True`, otherwise training will either silently # fail or completely fail. elif args.gradient_checkpointing: # For backward compatibility with older versions of transformers if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) if args.generate_during_eval and not (is_wandb_available() or is_comet_available()): raise ValueError( "`generate_during_eval=True` requires Weights and Biases or Comet to be installed." " Please install `wandb` or `comet-ml` to resolve." ) if model is not None: self.is_encoder_decoder = model.config.is_encoder_decoder elif args.is_encoder_decoder is None: raise ValueError("When no model is provided, you need to pass the parameter is_encoder_decoder.") else: self.is_encoder_decoder = args.is_encoder_decoder if self.is_encoder_decoder: self.decoder_start_token_id = model.config.decoder_start_token_id self.pad_token_id = model.config.pad_token_id if processing_class is None: raise ValueError("processing_class must be specified to tokenize a ORPO dataset.") if args.max_length is None: logger.warning( "`max_length` is not set in the ORPOConfig's init" " it will default to `512` by default, but you should do it yourself in the future.", ) max_length = 512 else: max_length = args.max_length if args.max_completion_length is None and self.is_encoder_decoder: logger.warning( "When using an encoder decoder architecture, you should set `max_completion_length` in the ORPOConfig's init" " it will default to `128` by default, but you should do it yourself in the future.", ) self.max_completion_length = 128 else: self.max_completion_length = args.max_completion_length if data_collator is None: data_collator = DPODataCollatorWithPadding( pad_token_id=processing_class.pad_token_id, is_encoder_decoder=self.is_encoder_decoder, ) if args.remove_unused_columns: args.remove_unused_columns = False # warn users logger.warning( "When using DPODataCollatorWithPadding, you should set `remove_unused_columns=False` in your TrainingArguments" " we have set it for you, but you should do it yourself in the future.", ) self.use_dpo_data_collator = True else: self.use_dpo_data_collator = False # Disable dropout in the model and reference model if args.disable_dropout: disable_dropout_in_model(model) self.max_length = max_length self.generate_during_eval = args.generate_during_eval self.padding_value = args.padding_value if args.padding_value is not None else processing_class.pad_token_id self.truncation_mode = args.truncation_mode self.processing_class = processing_class self.beta = args.beta self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) self.aux_loss_coef = getattr(model.config, "router_aux_loss_coef", 0.0) if self.aux_loss_enabled and self.aux_loss_coef == 0.0: logger.warning( "You set `output_router_logits` to `True` in the model config, but `router_aux_loss_coef` is set to " "`0.0`, meaning the auxiliary loss will not be used. Either set `router_aux_loss_coef` to a value " "greater than `0.0`, or set `output_router_logits` to `False` if you don't want to use the auxiliary " "loss.", ) self._stored_metrics = defaultdict(lambda: defaultdict(list)) # Compute that only on the main process for faster data processing. # see: https://github.com/huggingface/trl/pull/1255 with PartialState().main_process_first(): # Extract the prompt if needed, and apply the chat template if needed train_dataset = train_dataset.map(maybe_extract_prompt, num_proc=args.dataset_num_proc) train_dataset = train_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc ) train_dataset = train_dataset.map(self.tokenize_row, num_proc=args.dataset_num_proc) if eval_dataset is not None: eval_dataset = eval_dataset.map(maybe_extract_prompt, num_proc=args.dataset_num_proc) eval_dataset = eval_dataset.map( maybe_apply_chat_template, fn_kwargs={"tokenizer": processing_class}, num_proc=args.dataset_num_proc, ) eval_dataset = eval_dataset.map(self.tokenize_row, num_proc=args.dataset_num_proc) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, model_init=model_init, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) if not hasattr(self, "accelerator"): raise AttributeError( "Your `Trainer` does not have an `accelerator` object. Consider upgrading `transformers`." ) def build_tokenized_answer(self, prompt, answer): """ Llama tokenizer does satisfy `enc(a + b) = enc(a) + enc(b)`. It does ensure `enc(a + b) = enc(a) + enc(a + b)[len(enc(a)):]`. Reference: https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 """ full_tokenized = self.processing_class(prompt + answer, add_special_tokens=False) prompt_input_ids = self.processing_class(prompt, add_special_tokens=False)["input_ids"] answer_input_ids = full_tokenized["input_ids"][len(prompt_input_ids) :] answer_attention_mask = full_tokenized["attention_mask"][len(prompt_input_ids) :] # Concat tokens to form `enc(a) + enc(a + b)[len(enc(a)):]` full_concat_input_ids = np.concatenate([prompt_input_ids, answer_input_ids]) # Prepare input tokens for token by token comparison full_input_ids = np.array(full_tokenized["input_ids"]) if len(full_input_ids) != len(full_concat_input_ids): raise ValueError("Prompt input ids and answer input ids should have the same length.") # On some tokenizers, like Llama-2 tokenizer, there are occasions where tokens # can be merged together when tokenizing prompt+answer. This could result # on the last token from the prompt being different when tokenized on its own # vs when done as prompt+answer. response_token_ids_start_idx = len(prompt_input_ids) # If tokenized prompt is different than both prompt+answer, then it means the # last token has changed due to merging. if prompt_input_ids != full_tokenized["input_ids"][:response_token_ids_start_idx]: response_token_ids_start_idx -= 1 prompt_input_ids = full_tokenized["input_ids"][:response_token_ids_start_idx] prompt_attention_mask = full_tokenized["attention_mask"][:response_token_ids_start_idx] if len(prompt_input_ids) != len(prompt_attention_mask): raise ValueError("Prompt input ids and attention mask should have the same length.") answer_input_ids = full_tokenized["input_ids"][response_token_ids_start_idx:] answer_attention_mask = full_tokenized["attention_mask"][response_token_ids_start_idx:] return dict( prompt_input_ids=prompt_input_ids, prompt_attention_mask=prompt_attention_mask, input_ids=answer_input_ids, attention_mask=answer_attention_mask, ) def tokenize_row(self, feature, model: PreTrainedModel | nn.Module | None = None) -> dict: """Tokenize a single row from a ORPO specific dataset. At this stage, we don't convert to PyTorch tensors yet; we just handle the truncation in case the prompt + chosen or prompt + rejected responses is/are too long. First we truncate the prompt; if we're still too long, we truncate the chosen/rejected. We also create the labels for the chosen/rejected responses, which are of length equal to the sum of the length of the prompt and the chosen/rejected response, with `-100` for the prompt tokens. """ batch = {} prompt = feature["prompt"] chosen = feature["chosen"] rejected = feature["rejected"] if not self.is_encoder_decoder: # Check issues below for more details # 1. https://github.com/huggingface/trl/issues/907 # 2. https://github.com/EleutherAI/lm-evaluation-harness/pull/531#issuecomment-1595586257 # 3. https://github.com/LianjiaTech/BELLE/issues/337 if not isinstance(prompt, str): raise ValueError(f"prompt should be an str but got {type(prompt)}") prompt_tokens = self.processing_class(prompt, add_special_tokens=False) prompt_tokens = {f"prompt_{k}": v for k, v in prompt_tokens.items()} if not isinstance(chosen, str): raise ValueError(f"chosen should be an str but got {type(chosen)}") chosen_tokens = self.build_tokenized_answer(prompt, chosen) if not isinstance(rejected, str): raise ValueError(f"rejected should be an str but got {type(rejected)}") rejected_tokens = self.build_tokenized_answer(prompt, rejected) # Last prompt token might get merged by tokenizer and # it should not be included for generation if that happens prompt_len_input_ids = len(prompt_tokens["prompt_input_ids"]) chosen_prompt_len_input_ids = len(chosen_tokens["prompt_input_ids"]) rejected_prompt_len_input_ids = len(rejected_tokens["prompt_input_ids"]) prompt_len_input_ids = min(chosen_prompt_len_input_ids, rejected_prompt_len_input_ids) for k, v in prompt_tokens.items(): prompt_tokens[k] = v[:prompt_len_input_ids] # Make sure prompts only have one different token at most an # and length only differs by 1 at most num_diff_tokens = sum( a != b for a, b in zip(chosen_tokens["prompt_input_ids"], rejected_tokens["prompt_input_ids"], strict=False) ) num_diff_len = abs(chosen_prompt_len_input_ids - rejected_prompt_len_input_ids) if num_diff_tokens > 1 or num_diff_len > 1: raise ValueError( "Chosen and rejected prompt_input_ids might only differ on the " "last token due to tokenizer merge ops." ) # add BOS token to head of prompt. Avoid adding if it's already there prompt_tokens, chosen_tokens, rejected_tokens = add_bos_token_if_needed( self.processing_class.bos_token_id, prompt_len_input_ids, prompt_tokens, chosen_prompt_len_input_ids, chosen_tokens, rejected_prompt_len_input_ids, rejected_tokens, ) # add EOS token to end of answer. Avoid adding if it's already there chosen_tokens, rejected_tokens = add_eos_token_if_needed( self.processing_class.eos_token_id, chosen_tokens, rejected_tokens ) longer_response_length = max(len(chosen_tokens["input_ids"]), len(rejected_tokens["input_ids"])) # if combined sequence is too long, truncate the response for answer_tokens in [chosen_tokens, rejected_tokens]: if len(answer_tokens["prompt_input_ids"]) + longer_response_length > self.max_length: for k in ["input_ids", "attention_mask"]: answer_tokens[k] = answer_tokens[k][: self.max_length - longer_response_length] # Create labels chosen_sequence_tokens = { k: chosen_tokens[f"prompt_{k}"] + chosen_tokens[k] for k in ["input_ids", "attention_mask"] } rejected_sequence_tokens = { k: rejected_tokens[f"prompt_{k}"] + rejected_tokens[k] for k in ["input_ids", "attention_mask"] } chosen_sequence_tokens["labels"] = chosen_sequence_tokens["input_ids"][:] chosen_sequence_tokens["labels"][: len(chosen_tokens["prompt_input_ids"])] = [-100] * len( chosen_tokens["prompt_input_ids"] ) rejected_sequence_tokens["labels"] = rejected_sequence_tokens["input_ids"][:] rejected_sequence_tokens["labels"][: len(rejected_tokens["prompt_input_ids"])] = [-100] * len( rejected_tokens["prompt_input_ids"] ) for k, toks in { "chosen_": chosen_sequence_tokens, "rejected_": rejected_sequence_tokens, "": prompt_tokens, }.items(): for type_key, tokens in toks.items(): if type_key == "token_type_ids": continue batch[f"{k}{type_key}"] = tokens else: chosen_tokens = self.processing_class( chosen, truncation=True, max_length=self.max_completion_length, add_special_tokens=True ) rejected_tokens = self.processing_class( rejected, truncation=True, max_length=self.max_completion_length, add_special_tokens=True ) prompt_tokens = self.processing_class(prompt, add_special_tokens=True) batch["chosen_labels"] = chosen_tokens["input_ids"] batch["rejected_labels"] = rejected_tokens["input_ids"] batch["prompt_input_ids"] = prompt_tokens["input_ids"] batch["prompt_attention_mask"] = prompt_tokens["attention_mask"] if model is not None and hasattr(model, "prepare_decoder_input_ids_from_labels"): batch["rejected_decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels( labels=torch.tensor(batch["rejected_labels"]) ) batch["chosen_decoder_input_ids"] = model.prepare_decoder_input_ids_from_labels( labels=torch.tensor(batch["chosen_labels"]) ) if is_torch_xla_available(): # Pad the sequences to global max_length to avoid TorchXLA recompilation for k in batch: if "labels" in k or self.is_encoder_decoder: pad_value = -100 elif k.endswith("_input_ids"): pad_value = self.padding_value elif k.endswith("_attention_mask"): pad_value = 0 batch[k] = batch[k] + [pad_value] * (self.max_length - len(batch[k])) return batch @staticmethod def concatenated_inputs( batch: dict[str, list | torch.LongTensor], is_encoder_decoder: bool = False, padding_value: int = 0, device: torch.device | None = None, ) -> dict[str, torch.LongTensor]: """Concatenate the chosen and rejected inputs into a single tensor. Args: batch: A batch of data. Must contain the keys 'chosen_input_ids' and 'rejected_input_ids', which are tensors of shape (batch_size, sequence_length). is_encoder_decoder: Whether the model is an encoder-decoder model. padding_value: The padding value to use for the concatenated inputs_ids. device: The device for the concatenated inputs. Returns: A dictionary containing the concatenated inputs under the key 'concatenated_input_ids'. """ concatenated_batch = {} if is_encoder_decoder: max_length = max(batch["chosen_labels"].shape[1], batch["rejected_labels"].shape[1]) else: max_length = max(batch["chosen_input_ids"].shape[1], batch["rejected_input_ids"].shape[1]) for k in batch: if k.startswith("chosen") and isinstance(batch[k], torch.Tensor): if "labels" in k or is_encoder_decoder: pad_value = -100 elif k.endswith("_input_ids"): pad_value = padding_value elif k.endswith("_attention_mask"): pad_value = 0 concatenated_key = k.replace("chosen", "concatenated") concatenated_batch[concatenated_key] = pad_to_length(batch[k], max_length, pad_value=pad_value) for k in batch: if k.startswith("rejected") and isinstance(batch[k], torch.Tensor): if "labels" in k or is_encoder_decoder: pad_value = -100 elif k.endswith("_input_ids"): pad_value = padding_value elif k.endswith("_attention_mask"): pad_value = 0 concatenated_key = k.replace("rejected", "concatenated") concatenated_batch[concatenated_key] = torch.cat( ( concatenated_batch[concatenated_key], pad_to_length(batch[k], max_length, pad_value=pad_value), ), dim=0, ).to(device=device) if is_encoder_decoder: concatenated_batch["concatenated_input_ids"] = batch["prompt_input_ids"].repeat(2, 1).to(device=device) concatenated_batch["concatenated_attention_mask"] = ( batch["prompt_attention_mask"].repeat(2, 1).to(device=device) ) return concatenated_batch def odds_ratio_loss( self, policy_chosen_logps: torch.FloatTensor, policy_rejected_logps: torch.FloatTensor, ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Compute ORPO's odds ratio (OR) loss for a batch of policy and reference model log probabilities. Args: policy_chosen_logps: Log probabilities of the policy model for the chosen responses. Shape: (batch_size,) policy_rejected_logps: Log probabilities of the policy model for the rejected responses. Shape: (batch_size,) Returns: A tuple of three tensors: (losses, chosen_rewards, rejected_rewards). The losses tensor contains the ORPO loss for each example in the batch. The chosen_rewards and rejected_rewards tensors contain the rewards for the chosen and rejected responses, respectively. The log odds ratio of the chosen responses over the rejected responses ratio for logging purposes. The `log(sigmoid(log_odds_chosen))` for logging purposes. """ # Derived from Eqs. (4) and (7) from https://huggingface.co/papers/2403.07691 by using log identities and exp(log(P(y|x)) = P(y|x) policy_chosen_logps = policy_chosen_logps.float() policy_rejected_logps = policy_rejected_logps.float() log_odds = (policy_chosen_logps - policy_rejected_logps) - ( log1mexp(policy_chosen_logps) - log1mexp(policy_rejected_logps) ) ratio = F.logsigmoid(log_odds) losses = self.beta * ratio chosen_rewards = self.beta * (policy_chosen_logps.to(self.accelerator.device)).detach() rejected_rewards = self.beta * (policy_rejected_logps.to(self.accelerator.device)).detach() return losses, chosen_rewards, rejected_rewards, torch.mean(ratio), torch.mean(log_odds) @staticmethod def get_batch_logps( logits: torch.FloatTensor, labels: torch.LongTensor, average_log_prob: bool = False, is_encoder_decoder: bool = False, ) -> torch.FloatTensor: """Compute the log probabilities of the given labels under the given logits. Args: logits: Logits of the model (unnormalized). Shape: (batch_size, sequence_length, vocab_size) labels: Labels for which to compute the log probabilities. Label tokens with a value of `-100` are ignored. Shape: (batch_size, sequence_length) average_log_prob: If True, return the average log probability per (non-masked) token. Otherwise, return the sum of the log probabilities of the (non-masked) tokens. is_encoder_decoder: Whether the model is an encoder-decoder model. Returns: A tensor of shape (batch_size,) containing the average/sum log probabilities of the given labels under the given logits. """ if logits.shape[:-1] != labels.shape: raise ValueError("Logits (batch and sequence length dim) and labels must have the same shape.") if not is_encoder_decoder: labels = labels[:, 1:].clone() logits = logits[:, :-1, :] loss_mask = labels != -100 # dummy token; we'll ignore the losses on these tokens later labels = torch.where(labels == -100, 0, labels) per_token_logps = selective_log_softmax(logits, labels) if average_log_prob: return (per_token_logps * loss_mask).sum(-1) / loss_mask.sum(-1) else: return (per_token_logps * loss_mask).sum(-1) def concatenated_forward( self, model: nn.Module, batch: dict[str, list | torch.LongTensor] ) -> tuple[torch.FloatTensor, torch.FloatTensor, torch.FloatTensor, torch.FloatTensor]: """Run the given model on the given batch of inputs, concatenating the chosen and rejected inputs together. We do this to avoid doing two forward passes, because it's faster for FSDP. """ concatenated_batch = self.concatenated_inputs( batch, is_encoder_decoder=self.is_encoder_decoder, padding_value=self.padding_value, device=self.accelerator.device, ) len_chosen = batch["chosen_labels"].shape[0] model_kwargs = ( { "decoder_input_ids": self._shift_right(concatenated_batch["concatenated_labels"]), } if self.is_encoder_decoder else {} ) if self.aux_loss_enabled: model_kwargs["output_router_logits"] = True outputs = model( concatenated_batch["concatenated_input_ids"], attention_mask=concatenated_batch["concatenated_attention_mask"], use_cache=False, **model_kwargs, ) all_logits = outputs.logits def cross_entropy_loss(logits, labels): if not self.is_encoder_decoder: # Shift so that tokens < n predict n logits = logits[..., :-1, :].contiguous() labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() logits = logits.view(-1, logits.shape[-1]) labels = labels.view(-1) # Enable model parallelism labels = labels.to(logits.device) loss = loss_fct(logits, labels) return loss if self.is_encoder_decoder: labels = concatenated_batch["concatenated_labels"].clone() else: labels = concatenated_batch["concatenated_input_ids"].clone() attention_mask = concatenated_batch["concatenated_attention_mask"] labels = torch.where(attention_mask == 1, labels, -100) # orpo chosen nll loss is computed over the full prompt and response chosen_nll_loss = cross_entropy_loss(all_logits[:len_chosen], labels[:len_chosen]) all_logps = self.get_batch_logps( all_logits, concatenated_batch["concatenated_labels"], average_log_prob=True, is_encoder_decoder=self.is_encoder_decoder, ) chosen_logps = all_logps[:len_chosen] rejected_logps = all_logps[len_chosen:] if not self.is_encoder_decoder: chosen_logits = all_logits[:len_chosen, :-1, :] rejected_logits = all_logits[len_chosen:, :-1, :] else: chosen_logits = all_logits[:len_chosen] rejected_logits = all_logits[len_chosen:] if self.aux_loss_enabled: return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, chosen_nll_loss, outputs.aux_loss) return (chosen_logps, rejected_logps, chosen_logits, rejected_logits, chosen_nll_loss) def get_batch_loss_metrics( self, model, batch: dict[str, list | torch.LongTensor], train_eval: Literal["train", "eval"] = "train", ): """Compute the ORPO loss and other metrics for the given batch of inputs for train or test.""" metrics = {} forward_output = self.concatenated_forward(model, batch) ( policy_chosen_logps, policy_rejected_logps, policy_chosen_logits, policy_rejected_logits, policy_nll_loss, ) = forward_output[:5] if self.aux_loss_enabled: aux_loss = forward_output[5] losses, chosen_rewards, rejected_rewards, log_odds_ratio, log_odds_chosen = self.odds_ratio_loss( policy_chosen_logps, policy_rejected_logps ) # full ORPO loss loss = policy_nll_loss - losses.mean() reward_accuracies = (chosen_rewards > rejected_rewards).float() prefix = "eval_" if train_eval == "eval" else "" metrics[f"{prefix}rewards/chosen"] = self.accelerator.gather_for_metrics(chosen_rewards).mean() metrics[f"{prefix}rewards/rejected"] = self.accelerator.gather_for_metrics(rejected_rewards).mean() metrics[f"{prefix}rewards/accuracies"] = self.accelerator.gather_for_metrics(reward_accuracies).mean() metrics[f"{prefix}rewards/margins"] = self.accelerator.gather_for_metrics( chosen_rewards - rejected_rewards ).mean() metrics[f"{prefix}logps/rejected"] = self.accelerator.gather_for_metrics(policy_rejected_logps).detach().mean() metrics[f"{prefix}logps/chosen"] = self.accelerator.gather_for_metrics(policy_chosen_logps).detach().mean() metrics[f"{prefix}logits/rejected"] = self.accelerator.gather_for_metrics( policy_rejected_logits.detach().mean() ).mean() metrics[f"{prefix}logits/chosen"] = self.accelerator.gather_for_metrics( policy_chosen_logits.detach().mean() ).mean() metrics[f"{prefix}nll_loss"] = self.accelerator.gather_for_metrics(policy_nll_loss).detach().mean() metrics[f"{prefix}log_odds_ratio"] = self.accelerator.gather_for_metrics(log_odds_ratio).detach().mean() metrics[f"{prefix}log_odds_chosen"] = self.accelerator.gather_for_metrics(log_odds_chosen).detach().mean() if is_torch_xla_available(): xm.mark_step() # needed because .item() calls for k, v in metrics.items(): metrics[k] = v.item() if self.aux_loss_enabled: loss += self.aux_loss_coef * aux_loss return loss, metrics def compute_loss( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], return_outputs=False, num_items_in_batch=None, ) -> torch.Tensor | tuple[torch.Tensor, dict[str, torch.Tensor]]: compute_loss_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with compute_loss_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs, train_eval="train") # Make sure to move the loss to the device the original accumulating loss is at back in the `Trainer` class: loss = loss.to(self.args.device) # force log the metrics self.store_metrics(metrics, train_eval="train") if return_outputs: return (loss, metrics) return loss def generate_from_model(self, model, batch: dict[str, torch.LongTensor]) -> str: """Generate samples from the model and reference model for the given batch of inputs.""" # If one uses `generate_during_eval` with peft + bf16, we need to explicitly call generate with # the torch amp context manager as some hidden states are silently casted to full precision. generate_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with generate_context_manager: policy_output = model.generate( input_ids=batch["prompt_input_ids"], attention_mask=batch["prompt_attention_mask"], max_length=self.max_length, do_sample=True, pad_token_id=self.processing_class.pad_token_id, ) policy_output = pad_to_length(policy_output, self.max_length, self.processing_class.pad_token_id) policy_output_decoded = self.processing_class.batch_decode(policy_output, skip_special_tokens=True) return policy_output_decoded def prediction_step( self, model: PreTrainedModel | nn.Module, inputs: dict[str, torch.Tensor | Any], prediction_loss_only: bool, ignore_keys: list[str] | None = None, ): if not self.use_dpo_data_collator: logger.warning( "prediction_step is only implemented for DPODataCollatorWithPadding, and you passed a datacollator that is different than " "DPODataCollatorWithPadding - you might see unexpected behavior. Alternatively, you can implement your own prediction_step method if you are using a custom data collator" ) if ignore_keys is None: if hasattr(model, "config"): ignore_keys = getattr(model.config, "keys_to_ignore_at_inference", []) else: ignore_keys = [] prediction_context_manager = ( autocast(self.accelerator.device.type) if self._peft_has_been_casted_to_bf16 else nullcontext() ) with torch.no_grad(), prediction_context_manager: loss, metrics = self.get_batch_loss_metrics(model, inputs, train_eval="eval") # force log the metrics self.store_metrics(metrics, train_eval="eval") if prediction_loss_only: return (loss.detach(), None, None) # logits for the chosen and rejected samples from model logits_dict = { "eval_logits/chosen": metrics["eval_logits/chosen"], "eval_logits/rejected": metrics["eval_logits/rejected"], } logits = [v for k, v in logits_dict.items() if k not in ignore_keys] logits = torch.tensor(logits, device=self.accelerator.device) labels = torch.zeros(logits.shape[0], device=self.accelerator.device) return (loss.detach(), logits, labels) def store_metrics(self, metrics: dict[str, float], train_eval: Literal["train", "eval"] = "train") -> None: for key, value in metrics.items(): self._stored_metrics[train_eval][key].append(value) def evaluation_loop( self, dataloader: DataLoader, description: str, prediction_loss_only: bool | None = None, ignore_keys: list[str] | None = None, metric_key_prefix: str = "eval", ) -> EvalLoopOutput: """ Overriding built-in evaluation loop to store metrics for each batch. Prediction/evaluation loop, shared by `Trainer.evaluate()` and `Trainer.predict()`. Works both with or without labels. """ # Sample and save to game log if requested (for one batch to save time) if self.generate_during_eval: # Generate random indices within the range of the total number of samples num_samples = len(dataloader.dataset) random_indices = random.sample(range(num_samples), k=self.args.eval_batch_size) # Use dataloader.dataset.select to get the random batch without iterating over the DataLoader random_batch_dataset = dataloader.dataset.select(random_indices) random_batch = self.data_collator(random_batch_dataset) random_batch = self._prepare_inputs(random_batch) policy_output_decoded = self.generate_from_model(self.model, random_batch) table = pd.DataFrame( columns=["Prompt", "Policy"], data=[ [prompt, pol[len(prompt) :]] for prompt, pol in zip(random_batch["prompt"], policy_output_decoded, strict=True) ], ) if "wandb" in self.args.report_to: wandb.log({"game_log": wandb.Table(data=table)}) if "comet_ml" in self.args.report_to: log_table_to_comet_experiment( name="game_log.csv", table=table, ) # Base evaluation initial_output = super().evaluation_loop( dataloader, description, prediction_loss_only, ignore_keys, metric_key_prefix ) return initial_output def log(self, logs: dict[str, float], start_time: float | None = None) -> None: """ Log `logs` on the various objects watching training, including stored metrics. Args: logs (`dict[str, float]`): The values to log. start_time (`float`, *optional*): Start time of the training. """ # logs either has 'loss' or 'eval_loss' train_eval = "train" if "loss" in logs else "eval" # Add averaged stored metrics to logs for key, metrics in self._stored_metrics[train_eval].items(): logs[key] = torch.tensor(metrics).mean().item() del self._stored_metrics[train_eval] return super().log(logs, start_time) def _shift_right(self, input_ids): if self.decoder_start_token_id is None: raise ValueError( "model.config.decoder_start_token_id has to be defined. It is usually set to the pad_token_id." ) # shift inputs to the right if is_torch_fx_proxy(input_ids): # Item assignment is not supported natively for proxies. shifted_input_ids = torch.full(input_ids.shape[:-1] + (1,), self.decoder_start_token_id) shifted_input_ids = torch.cat([shifted_input_ids, input_ids[..., :-1]], dim=-1) else: shifted_input_ids = input_ids.new_zeros(input_ids.shape) shifted_input_ids[..., 1:] = input_ids[..., :-1].clone() shifted_input_ids[..., 0] = self.decoder_start_token_id if self.pad_token_id is None: raise ValueError("model.config.pad_token_id has to be defined.") # replace possible -100 values in labels by `pad_token_id` shifted_input_ids.masked_fill_(shifted_input_ids == -100, self.pad_token_id) return shifted_input_ids # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/papo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .papo_config import PAPOConfig from .papo_trainer import PAPOTrainer ================================================ FILE: trl/experimental/papo/papo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass from typing import Literal from ...trainer.grpo_config import GRPOConfig @dataclass class PAPOConfig(GRPOConfig): """ Configuration class for PAPOTrainer. PAPO (Perception-Aware Policy Optimization) extends GRPO/DAPO for multimodal reasoning by adding an implicit perception loss and double entropy regularization. Args: perception_loss_weight (`float`, *optional*, defaults to `0.1`): gamma Weight coefficient for the perception loss term. This encourages the model to be sensitive to visual changes. mask_ratio (`float`, *optional*, defaults to `0.3`): Ratio of the image to mask when computing perception loss. mask_type (`Literal["random", "patch", "grid"]`, *optional*, defaults to `"random"`): Type of masking strategy to use. der_loss_weight1 (`float`, *optional*, defaults to `0.03`): eta1 Weight coefficient for the Double Entropy Regularization (DER) term. This term encourages confident predictions with original images (low entropy) and uncertain predictions with masked images (high entropy). der_loss_weight2 (`float`, *optional*, defaults to `0.03`): eta2 Weight coefficient for the Double Entropy Regularization (DER) term. This term encourages confident predictions with original images (low entropy) and uncertain predictions with masked images (high entropy). loss_type (`Literal["grpo", "dapo"]`, inherited from GRPOConfig): Base loss type to use. Set to "grpo" for PAPO-G or "dapo" for PAPO-D. """ perception_loss_weight: float = 0.1 mask_ratio: float = 0.3 mask_type: Literal["random", "patch", "grid"] = "random" # Added for Double Entropy Regularization der_loss_weight1: float = 0.03 der_loss_weight2: float = 0.03 def __post_init__(self): super().__post_init__() # Validation if not 0.0 <= self.mask_ratio <= 1.0: raise ValueError(f"mask_ratio must be between 0 and 1, got {self.mask_ratio}") if self.der_loss_weight1 < 0 or self.der_loss_weight2 < 0: raise ValueError( f"der_loss_weight1 and der_loss_weight2 must be non-negative, got {self.der_loss_weight1} and {self.der_loss_weight2}" ) if self.mask_type not in ["random", "patch", "grid"]: raise ValueError(f"mask_type must be one of ['random', 'patch', 'grid'], got {self.mask_type}") ================================================ FILE: trl/experimental/papo/papo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import random import textwrap import torch from datasets import Dataset, IterableDataset from transformers import PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin from ...trainer.grpo_trainer import GRPOTrainer, RewardFunc from ...trainer.utils import nanmax, nanmin from .papo_config import PAPOConfig class PAPOTrainer(GRPOTrainer): """ Trainer for Perception-Aware Policy Optimization (PAPO). PAPO extends GRPO/DAPO for multimodal reasoning by adding an implicit perception loss that encourages the model to better utilize visual information. The key innovation is computing KL divergence between model outputs on original vs. corrupted (masked) images. Two variants are supported: - PAPO-G: PAPO + GRPO (use loss_type="grpo") - PAPO-D: PAPO + DAPO (use loss_type="dapo") Example: ```python from datasets import load_dataset from trl.experimental.papo import PAPOTrainer, PAPOConfig dataset = load_dataset("your-vlm-dataset", split="train") def reward_func(completions, **kwargs): # Your reward function for multimodal reasoning return [compute_reward(c) for c in completions] # PAPO-G config = PAPOConfig( loss_type="grpo", # Use GRPO as base perception_loss_weight=0.1, mask_ratio=0.3, ) # PAPO-G config = PAPOConfig( loss_type="dapo", # Use DAPO as base perception_loss_weight=0.1, mask_ratio=0.3, ) trainer = PAPOTrainer( model="Qwen/Qwen2-VL-2B-Instruct", reward_funcs=reward_func, args=config, train_dataset=dataset, ) trainer.train() ``` Args: model (`Union[str, PreTrainedModel]`): Model to be trained (must be a vision-language model). reward_funcs (`Union[RewardFunc, list[RewardFunc]]`): Reward functions for computing rewards (same as GRPO). args ([`PAPOConfig`], *optional*, defaults to `None`): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. Must include "prompt" and "image" columns. eval_dataset: Same requirements as train_dataset. processing_class: Processing class (tokenizer/processor) for the model. reward_processing_classes: Processing classes for reward models. callbacks: Training callbacks. optimizers: Optimizer and scheduler tuple. peft_config: PEFT configuration if using parameter-efficient fine-tuning. """ _tag_names = ["trl", "papo"] _name = "PAPO" _paper = { "title": "Perception-Aware Policy Optimization for Multimodal Reasoning", "id": "2507.06448", # docstyle-ignore "citation": textwrap.dedent( """\ @misc{wang2025perceptionawarepolicyoptimizationmultimodal, title = {{Perception-Aware Policy Optimization for Multimodal Reasoning}}, author = {Zhenhailong Wang and Xuehang Guo and Sofia Stoica and Haiyang Xu and Hongru Wang and Hyeonjeong Ha and Xiusi Chen and Yangyi Chen and Ming Yan and Fei Huang and Heng Ji}, year = 2025, url = {https://arxiv.org/abs/2507.06448}, archivePrefix= {arXiv}, eprint = {2507.06448}, primaryClass = {cs.CL} }""" ), } def __init__( self, model: str | PreTrainedModel, reward_funcs: RewardFunc | list[RewardFunc], args: PAPOConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, callbacks=None, optimizers=(None, None), peft_config=None, ): # Initialize with default PAPO config if not provided if args is None: model_name = model if isinstance(model, str) else model.config._name_or_path model_name = model_name.split("/")[-1] args = PAPOConfig(f"{model_name}-PAPO") # Store PAPO-specific parameters self.perception_loss_weight = args.perception_loss_weight self.mask_ratio = args.mask_ratio self.mask_type = args.mask_type self.der_loss_weight1 = args.der_loss_weight1 self.der_loss_weight2 = args.der_loss_weight2 # Initialize parent GRPO trainer super().__init__( model=model, reward_funcs=reward_funcs, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=reward_processing_classes, callbacks=callbacks, optimizers=optimizers, peft_config=peft_config, ) def _mask_image(self, pixel_values: torch.Tensor, mask_ratio: float = None) -> torch.Tensor: """ Apply masking to image pixel values. Args: pixel_values: Image tensor of shape (B, C, H, W) or (B, N, C, H, W) for multi-image mask_ratio: Ratio of image to mask (defaults to self.mask_ratio) Returns: Masked pixel values tensor """ if mask_ratio is None: mask_ratio = self.mask_ratio masked_pixel_values = pixel_values.clone() if self.mask_type == "random": # Random pixel masking mask = torch.rand_like(pixel_values) > mask_ratio masked_pixel_values = masked_pixel_values * mask elif self.mask_type == "patch": # Patch-based masking (mask contiguous regions) B = pixel_values.shape[0] if pixel_values.ndim == 4: # (B, C, H, W) C, H, W = pixel_values.shape[1:] for i in range(B): # Calculate patch size to mask patch_h = int(H * mask_ratio**0.5) patch_w = int(W * mask_ratio**0.5) # Random starting position start_h = random.randint(0, max(0, H - patch_h)) start_w = random.randint(0, max(0, W - patch_w)) # Apply mask masked_pixel_values[i, :, start_h : start_h + patch_h, start_w : start_w + patch_w] = 0 elif pixel_values.ndim == 5: # (B, N, C, H, W) for multi-image N, C, H, W = pixel_values.shape[1:] for i in range(B): for n in range(N): patch_h = int(H * mask_ratio**0.5) patch_w = int(W * mask_ratio**0.5) start_h = random.randint(0, max(0, H - patch_h)) start_w = random.randint(0, max(0, W - patch_w)) masked_pixel_values[i, n, :, start_h : start_h + patch_h, start_w : start_w + patch_w] = 0 elif self.mask_type == "grid": # Grid-based masking (mask regular grid cells) if pixel_values.ndim == 4: C, H, W = pixel_values.shape[1:] grid_size = int((1 / mask_ratio) ** 0.5) cell_h, cell_w = H // grid_size, W // grid_size for i in range(grid_size): for j in range(grid_size): if random.random() < mask_ratio: masked_pixel_values[:, :, i * cell_h : (i + 1) * cell_h, j * cell_w : (j + 1) * cell_w] = 0 return masked_pixel_values def _compute_loss(self, model, inputs): # >>> 1. GRPO loss # Compute the per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens # Compute the per_token_logps and the entropy at each position in the completion per_token_logps, entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, pixel_values=inputs.get("pixel_values"), image_grid_thw=inputs.get("image_grid_thw"), num_images=inputs.get("num_images"), pixel_attention_mask=inputs.get("pixel_attention_mask"), image_sizes=inputs.get("image_sizes"), ) if self.top_entropy_quantile < 1.0: entropy_mask = self.get_high_entropy_mask(entropies, completion_mask, 1 - self.top_entropy_quantile) else: entropy_mask = None # Compute the KL divergence between the model and the reference model if self.beta != 0.0: ref_per_token_logps = inputs["ref_per_token_logps"] per_token_kl = ( torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1 ) # Compute the loss advantages = inputs["advantages"] # When using num_iterations == 1 and steps_per_generation <= gradient_accumulation_steps # old_per_token_logps == per_token_logps, so we can skip it's computation # (see _generate_and_score_completions) and use per_token_logps.detach() instead. old_per_token_logps = inputs.get("old_per_token_logps") old_per_token_logps = per_token_logps.detach() if old_per_token_logps is None else old_per_token_logps log_ratio = per_token_logps - old_per_token_logps if self.importance_sampling_level == "token": log_importance_weights = log_ratio elif self.importance_sampling_level == "sequence": log_importance_weights = (log_ratio * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0) log_importance_weights = log_importance_weights.unsqueeze(-1) else: raise ValueError( f"Unknown importance sampling level: {self.importance_sampling_level}. Possible values are 'token' " "and 'sequence'." ) # From here, log_importance_weights (and all subsequent tensors, coef_1, coef_2, etc.) shape depends on # importance_sampling_level: "token" level: (B, T); "sequence" level: (B, 1) coef_1 = torch.exp(log_importance_weights) coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high) # Two-sided clipping if self.args.delta is not None: coef_1 = torch.clamp(coef_1, max=self.args.delta) per_token_loss1 = coef_1 * advantages.unsqueeze(1) per_token_loss2 = coef_2 * advantages.unsqueeze(1) per_token_loss = -torch.min(per_token_loss1, per_token_loss2) if entropy_mask is not None: per_token_loss = per_token_loss * entropy_mask if self.beta != 0.0: per_token_loss = per_token_loss + self.beta * per_token_kl if self.loss_type == "grpo": loss = ((per_token_loss * completion_mask).sum(-1) / completion_mask.sum(-1).clamp(min=1.0)).mean() loss = loss / self.current_gradient_accumulation_steps elif self.loss_type == "dapo": normalizer = inputs["num_items_in_batch"] / self.accelerator.num_processes loss = (per_token_loss * completion_mask).sum() / normalizer else: raise ValueError(f"Unknown loss type: {self.loss_type}") # >>> 2. Implicit Perception Loss inputs["pixel_values"] = self._mask_image(inputs["pixel_values"], self.mask_ratio) mask_img_per_token_logps, mask_img_entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, pixel_values=inputs.get("pixel_values"), image_grid_thw=inputs.get("image_grid_thw"), num_images=inputs.get("num_images"), pixel_attention_mask=inputs.get("pixel_attention_mask"), image_sizes=inputs.get("image_sizes"), ) perception_kl = ( torch.exp(mask_img_per_token_logps - per_token_logps) - (mask_img_per_token_logps - per_token_logps) - 1 ) perception_kl = torch.clamp(perception_kl, min=0.0, max=0.2) perception_loss = self.perception_loss_weight * perception_kl # >>> 3. Double Entropy Loss der_loss = self.der_loss_weight1 * entropies + self.der_loss_weight2 * mask_img_entropies # PAPO Loss loss = (loss - perception_loss + der_loss).mean() # Log the metrics mode = "train" if self.model.training else "eval" completion_token_count = completion_mask.sum().clamp(min=1.0) def masked_batch_mean(x): if x.shape[1] == 1: # when importance_sampling_level == "sequence" return x.mean() else: return (x * completion_mask).sum() / completion_token_count if self.beta != 0.0: mean_kl = masked_batch_mean(per_token_kl) self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).nanmean().item()) mean_entropy = masked_batch_mean(entropies) self._metrics[mode]["entropy"].append(self.accelerator.gather(mean_entropy).nanmean().item()) # Compute the clipped probability ratios is_low_clipped = (coef_1 < 1 - self.epsilon_low) & (advantages.unsqueeze(1) < 0) is_high_clipped = (coef_1 > 1 + self.epsilon_high) & (advantages.unsqueeze(1) > 0) is_region_clipped = is_low_clipped | is_high_clipped low_clip = masked_batch_mean(is_low_clipped.float()) high_clip = masked_batch_mean(is_high_clipped.float()) clip_ratio = masked_batch_mean(is_region_clipped.float()) gathered_low_clip = self.accelerator.gather(low_clip) self._metrics[mode]["clip_ratio/low_mean"].append(gathered_low_clip.nanmean().item()) self._metrics[mode]["clip_ratio/low_min"].append(nanmin(gathered_low_clip).item()) gathered_high_clip = self.accelerator.gather(high_clip) self._metrics[mode]["clip_ratio/high_mean"].append(gathered_high_clip.nanmean().item()) self._metrics[mode]["clip_ratio/high_max"].append(nanmax(gathered_high_clip).item()) gathered_clip_ratio = self.accelerator.gather(clip_ratio) self._metrics[mode]["clip_ratio/region_mean"].append(gathered_clip_ratio.nanmean().item()) return loss ================================================ FILE: trl/experimental/ppo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .modeling_value_head import ( AutoModelForCausalLMWithValueHead, AutoModelForSeq2SeqLMWithValueHead, PreTrainedModelWrapper, ) from .ppo_config import PPOConfig from .ppo_trainer import PPOTrainer __all__ = [ "AutoModelForCausalLMWithValueHead", "AutoModelForSeq2SeqLMWithValueHead", "PreTrainedModelWrapper", "PPOConfig", "PPOTrainer", ] ================================================ FILE: trl/experimental/ppo/modeling_value_head.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging import os import torch import torch.nn as nn from accelerate import PartialState from huggingface_hub import hf_hub_download from huggingface_hub.utils import ( EntryNotFoundError, HFValidationError, LocalEntryNotFoundError, RepositoryNotFoundError, ) from safetensors.torch import load_file as safe_load_file from transformers import ( AutoModelForCausalLM, AutoModelForSeq2SeqLM, PreTrainedModel, is_torch_npu_available, is_torch_xpu_available, ) from transformers.utils import is_peft_available if is_peft_available(): from peft import ( PeftConfig, PeftModel, PeftModelForCausalLM, PeftModelForSeq2SeqLM, PromptLearningConfig, get_peft_model, prepare_model_for_kbit_training, ) class PreTrainedModelWrapper(nn.Module): """ Wrapper for a [`~transformers.PreTrainedModel`] implemented as a standard PyTorch [`torch.nn.Module`]. This class provides a compatibility layer that preserves the key attributes and methods of the original [`~transformers.PreTrainedModel`], while exposing a uniform interface consistent with PyTorch modules. It enables seamless integration of pretrained Transformer models into custom training, evaluation, or inference workflows. Attributes: pretrained_model ([`~transformers.PreTrainedModel`]): The model to be wrapped. parent_class ([`~transformers.PreTrainedModel`]): The parent class of the model to be wrapped. supported_args (`list`): The list of arguments that are supported by the wrapper class. """ transformers_parent_class = None supported_args = None supported_modules = ("v_head",) supported_rm_modules = ("score",) supported_pretrained_model_architectures = ( (PreTrainedModel) if not is_peft_available() else (PreTrainedModel, PeftModelForCausalLM, PeftModelForSeq2SeqLM) ) def __init__( self, pretrained_model=None, score_module=None, supports_rm_adapter=False, rm_adapter_name=None, **kwargs ): super().__init__() self.pretrained_model = pretrained_model self.config = pretrained_model.config self.prepare_inputs_for_generation = pretrained_model.prepare_inputs_for_generation self.is_loaded_in_8bit = getattr(pretrained_model, "is_loaded_in_8bit", False) self.is_loaded_in_4bit = getattr(pretrained_model, "is_loaded_in_4bit", False) self.is_sequential_parallel = False if hasattr(pretrained_model, "gradient_checkpointing_disable"): self.gradient_checkpointing_disable = pretrained_model.gradient_checkpointing_disable if hasattr(pretrained_model, "gradient_checkpointing_enable"): self.gradient_checkpointing_enable = pretrained_model.gradient_checkpointing_enable if hasattr(pretrained_model, "enable_input_require_grads"): self.enable_input_require_grads = pretrained_model.enable_input_require_grads self.supports_rm_adapter = supports_rm_adapter self.rm_adapter_name = rm_adapter_name self.policy_adapter_name = "default" if score_module is not None: self.score = score_module @classmethod def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs): r""" Instantiates a new model from a pretrained model from `transformers`. The pretrained model is loaded using the `from_pretrained` method of the [`~transformers.PreTrainedModel`] class. The arguments that are specific to the [`~transformers.PreTrainedModel`] class are passed along this method and filtered out from the `kwargs` argument. Args: pretrained_model_name_or_path (`str` or [`~transformers.PreTrainedModel`]): The path to the pretrained model or its name. *model_args (`list`, *optional*): Additional positional arguments passed along to the underlying model's `from_pretrained` method. **kwargs (`dict`, *optional*): Additional keyword arguments passed along to the underlying model's `from_pretrained` method. We also pre-process the kwargs to extract the arguments that are specific to the [`~transformers.PreTrainedModel`] class and the arguments that are specific to trl models. The kwargs also support `prepare_model_for_kbit_training` arguments from `peft` library. """ if kwargs is not None: peft_config = kwargs.pop("peft_config", None) reward_adapter = kwargs.pop("reward_adapter", None) reward_adapter_name = kwargs.pop("reward_adapter_name", "reward_adapter") is_trainable = kwargs.pop("is_trainable", False) trl_model_args, pretrained_kwargs, peft_quantization_kwargs = cls._split_kwargs(kwargs) token = pretrained_kwargs.get("token", None) else: peft_config = None is_trainable = False trl_model_args = {} pretrained_kwargs = {} peft_quantization_kwargs = {} token = None if reward_adapter is not None and not isinstance(reward_adapter, str): raise ValueError( "The `reward_adapter` argument should be a string representing the name of local path or the Hub id to the Reward Modeling adapter." ) is_peft_model = False current_device = cls._get_current_device() if isinstance(pretrained_model_name_or_path, str): quantization_config = pretrained_kwargs.get("quantization_config", None) if quantization_config is not None: is_loaded_in_8bit = getattr(quantization_config, "load_in_8bit", False) is_loaded_in_4bit = getattr(quantization_config, "load_in_4bit", False) else: is_loaded_in_8bit = pretrained_kwargs["load_in_8bit"] if "load_in_8bit" in pretrained_kwargs else False is_loaded_in_4bit = pretrained_kwargs["load_in_4bit"] if "load_in_4bit" in pretrained_kwargs else False else: is_loaded_in_8bit = getattr(pretrained_model_name_or_path, "is_loaded_in_8bit", False) is_loaded_in_4bit = getattr(pretrained_model_name_or_path, "is_loaded_in_4bit", False) if (is_loaded_in_8bit or is_loaded_in_4bit) and "device_map" not in pretrained_kwargs: # warn users logging.warning( "The `device_map` argument is not provided. We will override the device_map argument." " to set the entire" " model on the current device. If you want to set the model on multiple devices, please provide" " a custom `device_map` argument." ) pretrained_kwargs["device_map"] = {"": current_device} if is_peft_available() and peft_config is not None and not isinstance(peft_config, PeftConfig): raise ValueError("The `peft_config` argument should be an instance of `peft.PeftConfig` class.") # First, load the pre-trained model using the parent-class # either `AutoModelForCausalLM` or `AutoModelForSeq2SeqLM` if isinstance(pretrained_model_name_or_path, str): if is_peft_available(): try: # If there is a trained peft adapter in the hub, load its config. remote_adapter_config = hf_hub_download( pretrained_model_name_or_path, "adapter_config.json", token=token, ) except (EntryNotFoundError, LocalEntryNotFoundError, HFValidationError, RepositoryNotFoundError): remote_adapter_config = None else: remote_adapter_config = None local_adapter_present = os.path.exists(os.path.join(pretrained_model_name_or_path, "adapter_config.json")) if (local_adapter_present or remote_adapter_config is not None) and is_peft_available(): if peft_config is not None: logging.warning( "`peft_config` argument ignored since a peft config file was found in " f"{pretrained_model_name_or_path}" ) # Load the trained peft adapter config if local_adapter_present: trained_adapter_config = PeftConfig.from_pretrained(pretrained_model_name_or_path) else: remote_adapter_dir = os.path.dirname(remote_adapter_config) trained_adapter_config = PeftConfig.from_pretrained(remote_adapter_dir) # Load the pretrained base model pretrained_model = cls.transformers_parent_class.from_pretrained( trained_adapter_config.base_model_name_or_path, *model_args, **pretrained_kwargs ) # Wrap the pretrained model with the trained peft adapter pretrained_model = PeftModel.from_pretrained( pretrained_model, pretrained_model_name_or_path, is_trainable=is_trainable, token=token ) logging.info("Trained peft adapter loaded") else: pretrained_model = cls.transformers_parent_class.from_pretrained( pretrained_model_name_or_path, *model_args, **pretrained_kwargs ) if peft_config is not None: # Initialize a new peft adapter with the given config if is_loaded_in_8bit or is_loaded_in_4bit: pretrained_model = prepare_model_for_kbit_training( pretrained_model, **peft_quantization_kwargs, ) pretrained_model = get_peft_model(pretrained_model, peft_config) logging.info("peft adapter initialised") elif isinstance(pretrained_model_name_or_path, cls.supported_pretrained_model_architectures): pretrained_model = pretrained_model_name_or_path if peft_config is not None and isinstance(pretrained_model, PreTrainedModel): # Initialize a new peft adapter with the given config if is_loaded_in_8bit or is_loaded_in_4bit: pretrained_model = prepare_model_for_kbit_training( pretrained_model, **peft_quantization_kwargs, ) pretrained_model = get_peft_model(pretrained_model, peft_config) logging.info("peft adapter initialised") else: raise ValueError( "pretrained_model_name_or_path should be a string or a PreTrainedModel, " f"but is {type(pretrained_model_name_or_path)}" ) if is_peft_available(): if isinstance(pretrained_model, PeftModel): is_peft_model = True # for backward compatibility if hasattr(pretrained_model, "active_peft_config") and isinstance( pretrained_model.active_peft_config, PromptLearningConfig ): raise ValueError("PromptLearningConfig is not supported for PPO training.") # Add reward modeling adapter if specified if not is_peft_model and reward_adapter is not None: raise ValueError("reward_adapter can only be used with a PeftModel. ") elif is_peft_model and reward_adapter is not None: score_module = cls.add_and_load_reward_modeling_adapter( pretrained_model, reward_adapter, reward_adapter_name, token=token ) multi_adapter_args = { "score_module": score_module, "supports_rm_adapter": True, "rm_adapter_name": reward_adapter_name, } else: multi_adapter_args = {"supports_rm_adapter": False} # Then, create the full model by instantiating the wrapper class model = cls(pretrained_model, **multi_adapter_args, **trl_model_args) # if resume_training, load the state_dict again - this is ok since the # state_dict is removed from the model after loading it. is_resuming_training = True if isinstance(pretrained_model_name_or_path, str): safe_filename = os.path.join(pretrained_model_name_or_path, "model.safetensors") filename = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin") sharded_index_filename = os.path.join(pretrained_model_name_or_path, "pytorch_model.bin.index.json") safe_sharded_index_filename = os.path.join(pretrained_model_name_or_path, "model.safetensors.index.json") is_sharded = False use_safe = os.path.exists(safe_filename) if not (os.path.exists(filename) or os.path.exists(safe_filename)): # Try with `pytorch_model.bin` filename, files_to_download, is_sharded, is_resuming_training = cls._get_checkpoint_from_hub( pretrained_model, pretrained_model_name_or_path, sharded_index_filename, token=token, ) # Try with safetensors if filename is None and files_to_download is None: safe_filename, files_to_download, is_sharded, is_resuming_training = cls._get_checkpoint_from_hub( pretrained_model, pretrained_model_name_or_path, safe_sharded_index_filename, token=token, model_name="model.safetensors", model_index_name="model.safetensors.index.json", ) use_safe = True else: use_safe = False loading_func = safe_load_file if use_safe else torch.load load_kwargs = {} if use_safe else {"map_location": "cpu", "weights_only": True} if is_resuming_training: if is_sharded: # download each file and add it to the state_dict state_dict = {} for shard_file in files_to_download: filename = hf_hub_download( pretrained_model_name_or_path, shard_file, token=token, ) state_dict.update(loading_func(filename, **load_kwargs)) else: state_dict = loading_func(filename if not use_safe else safe_filename, **load_kwargs) else: state_dict = pretrained_model_name_or_path.state_dict() model.is_peft_model = is_peft_model model.current_device = current_device if is_resuming_training: model.post_init(state_dict=state_dict) return model @classmethod def _get_checkpoint_from_hub( cls, pretrained_model, pretrained_model_name_or_path, index_filename, token=None, model_name="pytorch_model.bin", model_index_name="pytorch_model.bin.index.json", ): files_to_download = None filename = None is_resuming_training = True is_sharded = False try: filename = hf_hub_download( pretrained_model_name_or_path, model_name, token=token, ) # sharded except (EntryNotFoundError, LocalEntryNotFoundError, HFValidationError, RepositoryNotFoundError): if os.path.exists(index_filename): index_file_name = index_filename else: try: index_file_name = hf_hub_download( pretrained_model_name_or_path, model_index_name, token=token, ) except (EntryNotFoundError, LocalEntryNotFoundError, HFValidationError, RepositoryNotFoundError): # not continue training, do not have v_head weight is_resuming_training = False logging.warning( f"A {type(pretrained_model)} model is loaded from '{pretrained_model_name_or_path}', " f"and no v_head weight is found. This IS expected if you are not resuming PPO training." ) # load json if is_resuming_training: with open(index_file_name) as f: index = json.load(f) # check filename with `v_head` or any known extra module: files_to_download = set() for k, v in index["weight_map"].items(): if any(module in k for module in cls.supported_modules): files_to_download.add(v) is_sharded = True return filename, files_to_download, is_sharded, is_resuming_training @classmethod def _get_current_device(cls): r""" Get the current device. For GPU & XPU, we return the local process index using the `accelerate.PartialState` object to handle corner cases when running scripts in distributed environments. Returns: current_device (`int | str`): The current device. """ state = PartialState() if torch.cuda.is_available() or is_torch_xpu_available(): return state.local_process_index elif is_torch_npu_available(): return f"npu:{state.local_process_index}" else: return "cpu" @classmethod def _split_kwargs(cls, kwargs): """ Separate the kwargs from the arguments that we support inside `supported_args` and the ones that we don't. """ check_peft_kwargs = False if is_peft_available(): from peft import prepare_model_for_kbit_training check_peft_kwargs = True supported_kwargs = {} unsupported_kwargs = {} peft_kwargs = {} for key, value in kwargs.items(): if key in cls.supported_args: supported_kwargs[key] = value else: unsupported_kwargs[key] = value if check_peft_kwargs: if key in prepare_model_for_kbit_training.__code__.co_varnames: peft_kwargs[key] = value if key in unsupported_kwargs: unsupported_kwargs.pop(key) return supported_kwargs, unsupported_kwargs, peft_kwargs @classmethod def add_and_load_reward_modeling_adapter( cls, pretrained_model, adapter_model_id, adapter_name="reward_model_adapter", token=None ): r""" Add and load a reward modeling adapter. This method can only be used if the model is a `PeftModel` and if you have initialized the model with the `reward_modeling_adapter_id` argument, pointing to the id of the reward modeling adapter. The latest needs also to contain the score head in order to produce the reward. """ pretrained_model.load_adapter(adapter_model_id, adapter_name, is_trainable=False) pretrained_model.train() filename = os.path.join(adapter_model_id, "adapter_model.bin") safe_loading = False if not os.path.exists(filename): try: local_filename = hf_hub_download( adapter_model_id, "adapter_model.bin", token=token, ) except Exception: filename = os.path.join(adapter_model_id, "adapter_model.safetensors") safe_loading = True if not os.path.exists(filename): try: local_filename = hf_hub_download( adapter_model_id, "adapter_model.safetensors", token=token, ) except Exception as exc: raise ValueError( "Could not find adapter model in the Hub, make sure you have the correct adapter model id." ) from exc else: local_filename = filename else: local_filename = filename loading_func = safe_load_file if safe_loading else torch.load load_kwargs = {} if safe_loading else {"map_location": "cpu", "weights_only": True} adapter_state_dict = loading_func(local_filename, **load_kwargs) for score_name_candidate in cls.supported_rm_modules: if any(score_name_candidate in name for name in adapter_state_dict.keys()): score_name = score_name_candidate # we have found the correct head name and can break break score_dict = {} for name, param in adapter_state_dict.items(): if score_name in name: key_name = ".".join(name.split(".")[-1:]) score_dict[key_name] = param.to(cls._get_current_device()) num_labels, hidden_dim = score_dict["weight"].shape has_bias = any("bias" in name for name in adapter_state_dict.keys()) score = nn.Linear(hidden_dim, num_labels, bias=has_bias).to( device=cls._get_current_device(), dtype=pretrained_model.dtype, ) score.load_state_dict(score_dict) for param in score.parameters(): param.requires_grad = False return score def push_to_hub(self, *args, **kwargs): r""" Push the pretrained model to the hub. This method is a wrapper around [`~transformers.PreTrainedModel.push_to_hub`]. Please refer to the documentation of [`~transformers.PreTrainedModel.push_to_hub`] for more information. Args: *args (`list`, *optional*): Positional arguments passed along to the underlying model's `push_to_hub` method. **kwargs (`dict`, *optional*): Keyword arguments passed along to the underlying model's `push_to_hub` method. """ raise NotImplementedError def save_pretrained(self, *args, **kwargs): r""" Save the pretrained model to a directory. This method is a wrapper around [`~transformers.PreTrainedModel.save_pretrained`]. Please refer to the documentation of [`~transformers.PreTrainedModel.save_pretrained`] for more information. Args: *args (`list`, *optional*): Positional arguments passed along to the underlying model's `save_pretrained` method. **kwargs (`dict`, *optional*): Keyword arguments passed along to the underlying model's `save_pretrained` method. """ state_dict = kwargs.get("state_dict") if state_dict is None: state_dict = self.state_dict() kwargs["state_dict"] = state_dict # if it is a peft model only save the `v_head` state_dict and # pop the `state_dict` from the kwargs to avoid silent bugs with `peft` if self.is_peft_model: save_path = args[0] save_path = os.path.join(save_path, "pytorch_model.bin") torch.save(state_dict, save_path) _ = kwargs.pop("state_dict", None) return self.pretrained_model.save_pretrained(*args, **kwargs) def state_dict(self, *args, **kwargs): r""" Return the state_dict of the pretrained model. """ raise NotImplementedError def post_init(self, *args, **kwargs): r""" Post initialization method. This method is called after the model is instantiated and loaded from a checkpoint. It can be used to perform additional operations such as loading the state_dict. """ raise NotImplementedError def compute_reward_score(self, input_ids, attention_mask=None, **kwargs): r""" Computes the reward score for a given input. The method has first to enable the adapter and then compute the reward score. After that the model disables the reward modeling adapter and enables the default ppo adapter again. """ if not self.supports_rm_adapter: raise ValueError("This model does not support reward modeling adapter.") # enable rm adapter self.pretrained_model.set_adapter(self.rm_adapter_name) self.pretrained_model.eval() with torch.no_grad(): base_model_output = self.pretrained_model( input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True, return_dict=True, **kwargs, ) last_hidden_states = base_model_output.hidden_states[-1] scores = self.score(last_hidden_states) self.pretrained_model.set_adapter(self.policy_adapter_name) self.pretrained_model.eval() return scores class ValueHead(nn.Module): r""" The ValueHead class implements a head for GPT2 that returns a scalar for each output token. """ def __init__(self, config, **kwargs): super().__init__() if not hasattr(config, "summary_dropout_prob"): summary_dropout_prob = kwargs.pop("summary_dropout_prob", 0.1) else: summary_dropout_prob = config.summary_dropout_prob self.dropout = nn.Dropout(summary_dropout_prob) if summary_dropout_prob else nn.Identity() # some models such as OPT have a projection layer before the word embeddings - e.g. OPT-350m if hasattr(config, "hidden_size"): hidden_size = config.hidden_size if hasattr(config, "word_embed_proj_dim"): hidden_size = config.word_embed_proj_dim elif hasattr(config, "is_encoder_decoder"): if config.is_encoder_decoder and hasattr(config, "decoder"): if hasattr(config.decoder, "hidden_size"): hidden_size = config.decoder.hidden_size self.summary = nn.Linear(hidden_size, 1) self.flatten = nn.Flatten() def forward(self, hidden_states): output = self.dropout(hidden_states) # For now force upcast in fp32 if needed. Let's keep the # output in fp32 for numerical stability. if output.dtype != self.summary.weight.dtype: output = output.to(self.summary.weight.dtype) output = self.summary(output) return output class AutoModelForCausalLMWithValueHead(PreTrainedModelWrapper): """ An autoregressive model with a value head in addition to the language model head. This class inherits from [`experimental.ppo.PreTrainedModelWrapper`] and wraps a [`~transformers.PreTrainedModel`] class. The wrapper class supports classic functions such as `from_pretrained`, `push_to_hub` and `generate`. To call a method of the wrapped model, simply manipulate the `pretrained_model` attribute of this class. Class attributes: - **transformers_parent_class** ([`~transformers.PreTrainedModel`]) -- The parent class of the wrapped model. This should be set to `transformers.AutoModelForCausalLM` for this class. - **supported_args** (`tuple`) -- A tuple of strings that are used to identify the arguments that are supported by the [`ValueHead`] class. Currently, the supported args are: - **summary_dropout_prob** (`float`, `optional`, defaults to `None`) -- The dropout probability for the [`ValueHead`] class. - **v_head_initializer_range** (`float`, `optional`, defaults to `0.2`) -- The initializer range for the [`ValueHead`] if a specific initialization strategy is selected. - **v_head_init_strategy** (`str`, `optional`, defaults to `None`) -- The initialization strategy for the [`ValueHead`]. Currently, the supported strategies are: - **`None`** -- Initializes the weights of the [`ValueHead`] with a random distribution. This is the default strategy. - **"normal"** -- Initializes the weights of the [`ValueHead`] with a normal distribution. """ transformers_parent_class = AutoModelForCausalLM supported_args = ( "summary_dropout_prob", "v_head_initializer_range", "v_head_init_strategy", ) def __init__(self, pretrained_model, **kwargs): """ Initializes the model. Args: pretrained_model ([`~transformers.PreTrainedModel`]): The model to wrap. It should be a causal language model such as GPT2. or any model mapped inside the `AutoModelForCausalLM` class. kwargs (`dict`, `optional`): Additional keyword arguments, that are passed to the [`ValueHead`] class. """ super().__init__(pretrained_model, **kwargs) v_head_kwargs, _, _ = self._split_kwargs(kwargs) self.v_head = ValueHead(self.pretrained_model.config, **v_head_kwargs) self._init_weights(**v_head_kwargs) def _init_weights(self, **kwargs): r""" Initializes the weights of the value head. The default initialization strategy is random. Users can pass a different initialization strategy by passing the `v_head_init_strategy` argument when calling `.from_pretrained`. Supported strategies are: - `normal`: initializes the weights with a normal distribution. Args: **kwargs (`dict`, `optional`): Additional keyword arguments, that are passed to the [`ValueHead`] class. These arguments can contain the `v_head_init_strategy` argument as well as the `v_head_initializer_range` argument. """ initializer_range = kwargs.pop("v_head_initializer_range", 0.2) # random init by default init_strategy = kwargs.pop("v_head_init_strategy", None) if init_strategy is None: # do nothing pass elif init_strategy == "normal": self.v_head.summary.weight.data.normal_(mean=0.0, std=initializer_range) self.v_head.summary.bias.data.zero_() def forward( self, input_ids=None, past_key_values=None, attention_mask=None, return_past_key_values=False, **kwargs, ): r""" Applies a forward pass to the wrapped model and returns the logits of the value head. Args: input_ids (`torch.LongTensor` of shape `(batch_size, sequence_length)`): Indices of input sequence tokens in the vocabulary. past_key_values (`tuple(tuple(torch.FloatTensor))`, `optional`): Contains pre-computed hidden-states (key and values in the attention blocks) as computed by the model (see `past_key_values` input) to speed up sequential decoding. attention_mask (`torch.FloatTensor` of shape `(batch_size, sequence_length)`, `optional`): Mask to avoid performing attention on padding token indices. Mask values selected in ``[0, 1]``: - 1 for tokens that are **not masked**, - 0 for tokens that are **masked**. return_past_key_values (bool): A flag indicating if the computed hidden-states should be returned. kwargs (`dict`, `optional`): Additional keyword arguments, that are passed to the wrapped model. """ kwargs["output_hidden_states"] = True # this had already been set in the LORA / PEFT examples kwargs["past_key_values"] = past_key_values if self.is_peft_model and self.pretrained_model.active_peft_config.peft_type == "PREFIX_TUNING": kwargs.pop("past_key_values") base_model_output = self.pretrained_model( input_ids=input_ids, attention_mask=attention_mask, **kwargs, ) last_hidden_state = base_model_output.hidden_states[-1] lm_logits = base_model_output.logits loss = base_model_output.loss if last_hidden_state.device != self.v_head.summary.weight.device: last_hidden_state = last_hidden_state.to(self.v_head.summary.weight.device) value = self.v_head(last_hidden_state).squeeze(-1) # force upcast in fp32 if logits are in half-precision if lm_logits.dtype != torch.float32: lm_logits = lm_logits.float() if return_past_key_values: return (lm_logits, loss, value, base_model_output.past_key_values) else: return (lm_logits, loss, value) def generate(self, *args, **kwargs): r""" A simple wrapper around the `generate` method of the wrapped model. Please refer to the [`generate`](https://huggingface.co/docs/transformers/internal/generation_utils) method of the wrapped model for more information about the supported arguments. Args: *args (`list`, *optional*): Positional arguments passed to the `generate` method of the wrapped model. **kwargs (`dict`, *optional*): Keyword arguments passed to the `generate` method of the wrapped model. """ return self.pretrained_model.generate(*args, **kwargs) def state_dict(self, *args, **kwargs): r""" Returns the state dictionary of the model. We add the state dictionary of the value head to the state dictionary of the wrapped model by prepending the key with `v_head.`. """ if not self.is_peft_model: pretrained_model_state_dict = self.pretrained_model.state_dict(*args, **kwargs) else: # if it is a peft model, only save the v_head pretrained_model_state_dict = {} v_head_state_dict = self.v_head.state_dict(*args, **kwargs) for k, v in v_head_state_dict.items(): pretrained_model_state_dict[f"v_head.{k}"] = v return pretrained_model_state_dict def push_to_hub(self, *args, **kwargs): self.pretrained_model.v_head = self.v_head return self.pretrained_model.push_to_hub(*args, **kwargs) def post_init(self, state_dict): r""" We add the state dictionary of the value head to the state dictionary of the wrapped model by prepending the key with `v_head.`. This function removes the `v_head.` prefix from the keys of the value head state dictionary. """ for k in list(state_dict.keys()): if "v_head." in k: state_dict[k.replace("v_head.", "")] = state_dict.pop(k) self.v_head.load_state_dict(state_dict, strict=False) del state_dict if hasattr(self.pretrained_model, "hf_device_map"): if ( "cpu" in self.pretrained_model.hf_device_map.values() or "disk" in self.pretrained_model.hf_device_map.values() ): raise ValueError( "The model is offloaded on CPU or disk - CPU & disk offloading is not supported for ValueHead models." ) first_device = list(set(self.pretrained_model.hf_device_map.values()))[0] if isinstance(first_device, int): if is_torch_npu_available(): first_device = f"npu:{first_device}" elif is_torch_xpu_available(): first_device = f"xpu:{first_device}" else: first_device = f"cuda:{first_device}" self.v_head = self.v_head.to(first_device) def set_device_hook(module, input, outputs): new_output = () for output in outputs: if isinstance(output, torch.Tensor): new_output += (output.to(first_device),) else: new_output += (output,) return new_output self.register_forward_hook(set_device_hook) self.is_sequential_parallel = True class AutoModelForSeq2SeqLMWithValueHead(PreTrainedModelWrapper): """ A seq2seq model with a value head in addition to the language model head. This class inherits from [`experimental.ppo.PreTrainedModelWrapper`] and wraps a [`~transformers.PreTrainedModel`] class. The wrapper class supports classic functions such as `from_pretrained` and `push_to_hub` and also provides some additional functionalities such as `generate`. Args: pretrained_model ([`~transformers.PreTrainedModel`]): The model to wrap. It should be a causal language model such as GPT2. or any model mapped inside the [`~transformers.AutoModelForSeq2SeqLM`] class. kwargs: Additional keyword arguments passed along to the [`ValueHead`] class. """ transformers_parent_class = AutoModelForSeq2SeqLM lm_head_namings = ["lm_head", "embed_out", "output_projection"] supported_args = ( "summary_dropout_prob", "v_head_initializer_range", "v_head_init_strategy", ) def __init__(self, pretrained_model, **kwargs): super().__init__(pretrained_model, **kwargs) v_head_kwargs, _, _ = self._split_kwargs(kwargs) self.is_encoder_decoder = True if not self._has_lm_head(): raise ValueError("The model does not have a language model head, please use a model that has one.") self.v_head = ValueHead(self.pretrained_model.config, **v_head_kwargs) self._init_weights(**v_head_kwargs) def _has_lm_head(self): # check module names of all modules inside `pretrained_model` to find the language model head for name, _module in self.pretrained_model.named_modules(): if any(attribute in name for attribute in self.lm_head_namings): return True return False def post_init(self, state_dict): r""" We add the state dictionary of the value head to the state dictionary of the wrapped model by prepending the key with `v_head.`. This function removes the `v_head.` prefix from the keys of the value head state dictionary. """ for k in list(state_dict.keys()): if "v_head." in k: state_dict[k.replace("v_head.", "")] = state_dict.pop(k) self.v_head.load_state_dict(state_dict, strict=False) del state_dict if hasattr(self.pretrained_model, "hf_device_map"): if ( "cpu" in self.pretrained_model.hf_device_map.values() or "disk" in self.pretrained_model.hf_device_map.values() ): raise ValueError( "The model is offloaded on CPU or disk - CPU & disk offloading is not supported for ValueHead models." ) # get the lm_head device for name, module in self.pretrained_model.named_modules(): if any(attribute in name for attribute in self.lm_head_namings): lm_head_device = module.weight.device break # put v_head on the same device as the lm_head to avoid issues self.v_head = self.v_head.to(lm_head_device) def set_device_hook(module, input, outputs): r""" A hook that sets the device of the output of the model to the device of the first parameter of the model. Args: module (`nn.Module`): The module to which the hook is attached. input (`tuple`): The input to the module. outputs (`tuple`): The output of the module. """ new_output = () for output in outputs: if isinstance(output, torch.Tensor): new_output += (output.to(lm_head_device),) else: new_output += (output,) return new_output self.register_forward_hook(set_device_hook) self.is_sequential_parallel = True def state_dict(self, *args, **kwargs): r""" Returns the state dictionary of the model. We add the state dictionary of the value head to the state dictionary of the wrapped model by prepending the key with `v_head.`. """ if not self.is_peft_model: pretrained_model_state_dict = self.pretrained_model.state_dict(*args, **kwargs) else: # if it is a peft model, only save the v_head pretrained_model_state_dict = {} v_head_state_dict = self.v_head.state_dict(*args, **kwargs) for k, v in v_head_state_dict.items(): pretrained_model_state_dict[f"v_head.{k}"] = v return pretrained_model_state_dict def push_to_hub(self, *args, **kwargs): self.pretrained_model.v_head = self.v_head return self.pretrained_model.push_to_hub(*args, **kwargs) def _init_weights(self, **kwargs): r""" We initialize the weights of the value head. """ initializer_range = kwargs.pop("v_head_initializer_range", 0.2) # random init by default init_strategy = kwargs.pop("v_head_init_strategy", None) if init_strategy is None: # do nothing pass elif init_strategy == "normal": self.v_head.summary.weight.data.normal_(mean=0.0, std=initializer_range) self.v_head.summary.bias.data.zero_() def forward( self, input_ids=None, past_key_values=None, attention_mask=None, return_past_key_values=False, **kwargs, ): kwargs["past_key_values"] = past_key_values if self.is_peft_model and self.pretrained_model.active_peft_config.peft_type == "PREFIX_TUNING": kwargs.pop("past_key_values") base_model_output = self.pretrained_model( input_ids=input_ids, attention_mask=attention_mask, output_hidden_states=True, # We force the model to output hidden states **kwargs, ) last_hidden_state = base_model_output.decoder_hidden_states[-1] lm_logits = base_model_output.logits loss = base_model_output.loss value = self.v_head(last_hidden_state).squeeze(-1) # force upcast in fp32 if logits are in half-precision if lm_logits.dtype != torch.float32: lm_logits = lm_logits.float() if return_past_key_values: return (lm_logits, loss, value, base_model_output.past_key_values) else: return (lm_logits, loss, value) def generate(self, *args, **kwargs): r""" We call `generate` on the wrapped model. """ return self.pretrained_model.generate(*args, **kwargs) ================================================ FILE: trl/experimental/ppo/ppo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Literal from ...trainer.base_config import _BaseConfig @dataclass class PPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.ppo.PPOTrainer`]. This class includes only the parameters that are specific to PPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. num_mini_batches (`int`, *optional*, defaults to `1`): Number of minibatches to split a batch into. total_episodes (`int`, *optional*): Total number of episodes in the dataset. local_rollout_forward_batch_size (`int`, *optional*, defaults to `64`): Per rank no grad forward pass in the rollout phase. num_sample_generations (`int`, *optional*, defaults to `10`): Number of debugging samples generations (i.e., `generate_completions` calls) throughout training. response_length (`int`, *optional*, defaults to `53`): Length of the response. stop_token (`str`, *optional*): Specifies the stop token to use for text generation. This parameter is mutually exclusive with `stop_token_id`. - `None`: No stop token is applied, unless `stop_token_id` is specified. - `'eos'`: Uses the tokenizer's `eos_token`. stop_token_id (`int`, *optional*): Specifies the ID of the stop token to use for text generation. If `None`, no stop token ID is applied, unless `stop_token` is specified. This parameter is mutually exclusive with `stop_token`. temperature (`float`, *optional*, defaults to `0.7`): Sampling temperature. missing_eos_penalty (`float`, *optional*): Penalty applied to the score when the model fails to generate an EOS token. This is useful to encourage to generate completions shorter than the maximum length (`max_new_tokens`). The penalty must be a positive value. sft_model_path (`str`, *optional*, defaults to `"EleutherAI/pythia-160m"`): Path to the SFT model. world_size (`int`, *optional*): Number of processes (GPUs) to use for the training. num_total_batches (`int`, *optional*): Number of total batches to train. micro_batch_size (`int`, *optional*): Micro batch size across devices (HF's `per_device_train_batch_size` * `world_size`). local_batch_size (`int`, *optional*): Batch size per GPU (HF's `per_device_train_batch_size` * `gradient_accumulation_steps`). batch_size (`int`, *optional*): Batch size across devices (HF's `per_device_train_batch_size` * `world_size` * `gradient_accumulation_steps`). local_mini_batch_size (`int`, *optional*): Mini batch size per GPU. mini_batch_size (`int`, *optional*): Mini batch size across GPUs. push_to_hub (`bool`, *optional*, defaults to `False`): Whether to push the model to the Hub after training. reward_model_path (`str`, *optional*, defaults to `"EleutherAI/pythia-160m"`): Path to the reward model. model_adapter_name (`str`, *optional*): Name of the train target PEFT adapter, when using LoRA with multiple adapters. ref_adapter_name (`str`, *optional*): Name of the reference PEFT adapter, when using LoRA with multiple adapters. num_ppo_epochs (`int`, *optional*, defaults to `4`): Number of epochs to train. whiten_rewards (`bool`, *optional*, defaults to `False`): Whether to whiten the rewards. kl_coef (`float`, *optional*, defaults to `0.05`): KL coefficient. kl_estimator (`Literal["k1", "k3"]`, *optional*, defaults to `"k1"`): Which estimator for KL-Divergence to use from [Approximating KL Divergence](http://joschu.net/blog/kl-approx.html). Defaults to "k1", a straightforward, unbiased estimator. Can be set to "k3", an unbiased estimator with lower variance which "appears to be a strictly better estimator". Cannot be set to "k2", as it is used for logging purposes. cliprange (`float`, *optional*, defaults to `0.2`): Clip range. vf_coef (`float`, *optional*, defaults to `0.1`): Value function coefficient. cliprange_value (`float`, *optional*, defaults to `0.2`): Clip range for the value function. gamma (`float`, *optional*, defaults to `1.0`): Discount factor. lam (`float`, *optional*, defaults to `0.95`): Lambda value for GAE. ds3_gather_for_generation (`bool`, *optional*, defaults to `True`): This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for generation, improving generation speed. However, disabling this option allows training models that exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `3e-6` instead of `5e-5`. """ # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=3e-6, metadata={"help": "The initial learning rate for AdamW."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) num_mini_batches: int = field( default=1, metadata={"help": "Number of minibatches to split a batch into."}, ) total_episodes: int | None = field( default=None, metadata={"help": "Total number of episodes in the dataset."}, ) local_rollout_forward_batch_size: int = field( default=64, metadata={"help": "Per rank no grad forward pass in the rollout phase."}, ) num_sample_generations: int = field( default=10, metadata={ "help": "Number of debugging samples generations (i.e., `generate_completions` calls) throughout training." }, ) response_length: int = field( default=53, metadata={"help": "Length of the response."}, ) stop_token: Literal["eos"] | None = field( default=None, metadata={ "help": "Specifies the stop token to use for text generation. This parameter is mutually exclusive with " "`stop_token_id`." }, ) stop_token_id: int | None = field( default=None, metadata={ "help": "Specifies the ID of the stop token to use for text generation. If `None`, no stop token ID is " "applied, unless `stop_token` is specified. This parameter is mutually exclusive with `stop_token`." }, ) temperature: float = field( default=0.7, metadata={"help": "Sampling temperature."}, ) missing_eos_penalty: float | None = field( default=None, metadata={ "help": "Penalty applied to the score when the model fails to generate an EOS token. This is useful to " "encourage to generate completions shorter than the maximum length (`max_new_tokens`). The penalty must be " "a positive value." }, ) sft_model_path: str = field( default="EleutherAI/pythia-160m", metadata={"help": "Path to the SFT model."}, ) world_size: int | None = field( default=None, metadata={"help": "Number of processes (GPUs) to use for the training."}, ) num_total_batches: int | None = field( default=None, metadata={"help": "Number of total batches to train."}, ) micro_batch_size: int | None = field( default=None, metadata={"help": "Micro batch size across devices (HF's `per_device_train_batch_size` * `world_size`)."}, ) local_batch_size: int | None = field( default=None, metadata={"help": "Batch size per GPU (HF's `per_device_train_batch_size` * `gradient_accumulation_steps`)."}, ) batch_size: int | None = field( default=None, metadata={ "help": "Batch size across devices (HF's `per_device_train_batch_size` * `world_size` * " "`gradient_accumulation_steps`)." }, ) local_mini_batch_size: int | None = field( default=None, metadata={"help": "Mini batch size per GPU."}, ) mini_batch_size: int | None = field( default=None, metadata={"help": "Mini batch size across GPUs."}, ) push_to_hub: bool = field( default=False, metadata={"help": "Whether to push the model to the Hub after training."}, ) reward_model_path: str = field( default="EleutherAI/pythia-160m", metadata={"help": "Path to the reward model."}, ) model_adapter_name: str | None = field( default=None, metadata={"help": "Name of the train target PEFT adapter, when using LoRA with multiple adapters."}, ) ref_adapter_name: str | None = field( default=None, metadata={"help": "Name of the reference PEFT adapter, when using LoRA with multiple adapters."}, ) num_ppo_epochs: int = field( default=4, metadata={"help": "Number of epochs to train."}, ) whiten_rewards: bool = field( default=False, metadata={"help": "Whether to whiten the rewards."}, ) kl_coef: float = field( default=0.05, metadata={"help": "KL coefficient."}, ) kl_estimator: Literal["k1", "k3"] = field( default="k1", metadata={ "help": "Which estimator for KL-Divergence to use from Approximating KL Divergence " "(http://joschu.net/blog/kl-approx.html). Defaults to 'k1', a straightforward, unbiased estimator. Can be " "set to 'k3', an unbiased estimator with lower variance which 'appears to be a strictly better " "estimator'. Cannot be set to 'k2', as it is used for logging purposes." }, ) cliprange: float = field( default=0.2, metadata={"help": "Clip range."}, ) vf_coef: float = field( default=0.1, metadata={"help": "Value function coefficient."}, ) cliprange_value: float = field( default=0.2, metadata={"help": "Clip range for the value function."}, ) gamma: float = field( default=1.0, metadata={"help": "Discount factor."}, ) lam: float = field( default=0.95, metadata={"help": "Lambda value for GAE."}, ) ds3_gather_for_generation: bool = field( default=True, metadata={ "help": "This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for " "generation, improving generation speed. However, disabling this option allows training models that " "exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation." }, ) ================================================ FILE: trl/experimental/ppo/ppo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gc import math import os import textwrap import time from collections import defaultdict from contextlib import contextmanager, nullcontext from dataclasses import dataclass from pathlib import Path import numpy as np import pandas as pd import torch import torch.nn as nn import transformers from accelerate import Accelerator, logging from accelerate.utils import gather_object from datasets import Dataset from packaging.version import Version from torch.utils.data import DataLoader from transformers import ( BaseImageProcessor, DataCollatorWithPadding, FeatureExtractionMixin, GenerationConfig, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, TrainerControl, TrainerState, ) from transformers.integrations import get_reporting_integration_callbacks from transformers.trainer import DEFAULT_CALLBACKS, DEFAULT_PROGRESS_CALLBACK from transformers.trainer_callback import CallbackHandler, ExportableState, PrinterCallback from transformers.utils import ModelOutput, is_peft_available, is_rich_available from ...models.utils import unwrap_model_for_generation from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import ( disable_dropout_in_model, log_table_to_comet_experiment, pad, prepare_deepspeed, selective_log_softmax, ) from ..utils import ( create_reference_model, empty_cache, first_true_indices, get_reward, peft_module_casting_to_bf16, ) from .ppo_config import PPOConfig if is_rich_available(): from rich.console import Console from rich.table import Table logger = logging.get_logger(__name__) if is_peft_available(): from peft import PeftConfig, PeftModel, get_peft_model INVALID_LOGPROB = 1.0 def generate( lm_backbone: torch.nn.Module, queries: torch.Tensor, pad_token_id: int, generation_config: GenerationConfig ) -> tuple[torch.Tensor, torch.Tensor]: """ Generates sequences from the language model backbone in a way that does not affect padding tokens. Args: lm_backbone (`torch.nn.Module`): The language model backbone used for generation. queries (`torch.Tensor`): The tensor containing the input queries. pad_token_id (`int`): The token ID representing the pad token. generation_config ([`~transformers.GenerationConfig`]): The configuration for the generation process. Returns: tuple: - `generated_sequences` (`torch.Tensor`): The concatenated tensor of input queries and generated sequences. - `logits` (`torch.Tensor`): The logits output from the generation process. """ context_length = queries.shape[1] attention_mask = queries != pad_token_id input_ids = torch.masked_fill(queries, ~attention_mask, 0) output = lm_backbone.generate( input_ids=input_ids, attention_mask=attention_mask, # position_ids=attention_mask.cumsum(1) - attention_mask.long(), # not needed: already adjusted in generations # https://github.com/huggingface/transformers/blob/ac33aeeeee2a7a89b89c93c2962e6feb90daef0a/src/transformers/models/gpt2/modeling_gpt2.py#L1227-L1250 generation_config=generation_config, return_dict_in_generate=True, output_scores=True, ) logits = torch.stack(output.scores, 1) return torch.cat((queries, output.sequences[:, context_length:]), dim=1), logits @torch.no_grad() def batch_generation( model: torch.nn.Module, queries: torch.Tensor, local_rollout_forward_batch_size: int, pad_token_id: int, generation_config: GenerationConfig, ): query_responses = [] logitss = [] batch_size = queries.shape[0] for i in range(0, batch_size, local_rollout_forward_batch_size): query = queries[i : i + local_rollout_forward_batch_size] query_response, logits = generate( model, query, pad_token_id, generation_config, ) query_responses.append(query_response) logitss.append(logits) # padding tensors padded_query_responses = pad(query_responses, padding_value=pad_token_id, padding_side="right") padded_logitss = pad(logitss, padding_value=0, padding_side="right") # reshaping padded_query_responses = padded_query_responses.view(-1, padded_query_responses.shape[-1])[:batch_size] padded_logitss = padded_logitss.view(-1, *padded_logitss.shape[2:])[:batch_size] return padded_query_responses, padded_logitss def exact_div(a, b, custom_error_message=""): q = a // b if a != q * b: raise ValueError(f"{custom_error_message}, inexact division: {a} / {b} = {a / b}") return q def print_rich_table(df: pd.DataFrame) -> None: if not is_rich_available(): raise ImportError( "The function `print_rich_table` requires the `rich` library. Please install it with `pip install rich`." ) console = Console() table = Table(show_lines=True) for column in df.columns: table.add_column(column) for _, row in df.iterrows(): table.add_row(*row.astype(str).tolist()) console.print(table) def truncate_response(stop_token_id: int, pad_token_id: int, responses: torch.Tensor) -> torch.Tensor: """ Truncates the responses at the first occurrence of the stop token, filling the rest with pad tokens. Args: stop_token_id (`int`): The token ID representing the stop token where truncation occurs. pad_token_id (`int`): The token ID representing the pad token used to fill the truncated responses. responses (`torch.Tensor`): The tensor containing the responses to be truncated. Returns: `torch.Tensor`: The truncated responses tensor with pad tokens filled after the stop token. """ trunc_idxs = first_true_indices(responses == stop_token_id).unsqueeze(-1) new_size = [1] * (len(responses.size()) - 1) + [responses.shape[1]] idxs = torch.arange(responses.shape[1], device=responses.device).view(*new_size) postprocessed_responses = torch.masked_fill(responses, idxs > trunc_idxs, pad_token_id) return postprocessed_responses def forward( model: torch.nn.Module, query_responses: torch.Tensor, pad_token_id: int, ) -> ModelOutput: """ Performs a forward pass through the model with the given query responses and pad token ID. Args: model (`torch.nn.Module`): The model to perform the forward pass. query_responses (`torch.Tensor`): The tensor containing the query responses. pad_token_id (`int`): The token ID representing the pad token. Returns: `ModelOutput`: The output of the model, including hidden states. """ attention_mask = query_responses != pad_token_id position_ids = attention_mask.cumsum(1) - attention_mask.long() input_ids = torch.masked_fill(query_responses, ~attention_mask, 0) return model( input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids, return_dict=True, output_hidden_states=True, ) @dataclass class OnlineTrainerState(TrainerState): """ Training state for online/on-policy trainers. Extends [`~transformers.TrainerState`] with an `episode` counter to track the current rollout/episode. Args: episode (`int`, defaults to 0): Zero-based episode index. """ episode: int = 0 def masked_mean(values: torch.Tensor, mask: torch.Tensor, axis: bool | None = None) -> torch.Tensor: """Compute mean of tensor with a masked values.""" if axis is not None: return (values * mask).sum(axis=axis) / mask.sum(axis=axis) else: return (values * mask).sum() / mask.sum() def masked_var(values: torch.Tensor, mask: torch.Tensor, unbiased: bool = True) -> torch.Tensor: """Compute variance of tensor with masked values.""" mean = masked_mean(values, mask) centered_values = values - mean variance = masked_mean(centered_values**2, mask) if unbiased: mask_sum = mask.sum() if mask_sum == 0: raise ValueError( "The sum of the mask is zero, which can happen when `mini_batch_size=1`;" "try increase the `mini_batch_size` or `gradient_accumulation_steps`" ) # note that if mask_sum == 1, then there is a division by zero issue # to avoid it you just need to use a larger minibatch_size bessel_correction = mask_sum / (mask_sum - 1) variance = variance * bessel_correction return variance def masked_whiten(values: torch.Tensor, mask: torch.Tensor, shift_mean: bool = True) -> torch.Tensor: """Whiten values with masked values.""" mean, var = masked_mean(values, mask), masked_var(values, mask) whitened = (values - mean) * torch.rsqrt(var + 1e-8) if not shift_mean: whitened += mean return whitened # taken from https://github.com/OpenLMLab/MOSS-RLHF/blob/40b91eb2f2b71b16919addede0341d2bef70825d/ppo/ppo_trainer.py#L29 # we did this we can do a single `model = accelerator.prepare(model)` class PolicyAndValueWrapper(nn.Module): def __init__(self, policy, value_model) -> None: super().__init__() self.policy = policy self.value_model = value_model self.critic_backbone = getattr(value_model, value_model.base_model_prefix) self.is_gradient_checkpointing = policy.is_gradient_checkpointing def gradient_checkpointing_enable(self, **kwargs): self.policy.gradient_checkpointing_enable(**kwargs) self.is_gradient_checkpointing = True def gradient_checkpointing_disable(self): self.policy.gradient_checkpointing_disable() self.is_gradient_checkpointing = False def forward(self, **kwargs): output = self.critic_backbone(**kwargs) logits = self.value_model.score(output.hidden_states[-1]) return self.policy(**kwargs), logits class PPOTrainer(_BaseTrainer): """Trainer for Proximal Policy Optimization (PPO). For details on PPO, see the paper: [Proximal Policy Optimization Algorithms](https://huggingface.co/papers/1707.06347). Args: args ([`experimental.ppo.PPOConfig`]): Training arguments. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`]): Class to process the data. model (`torch.nn.Module`): Model to be trained. This is the policy model. ref_model (`torch.nn.Module`, *optional*): Reference model used to compute the KL divergence. If `None`, a copy of the policy model is created. reward_model (`torch.nn.Module`): Reward model used to compute the rewards. train_dataset ([`~datasets.Dataset`]): Dataset for training. value_model (`torch.nn.Module`): Value model used to predict the value of a state. data_collator ([`~transformers.DataCollatorWithPadding`], *optional*): Data collator to batch and pad samples from the dataset. If `None`, a default data collator is created using the `processing_class`. eval_dataset ([`~datasets.Dataset`] or `dict` of [`~datasets.Dataset`], *optional*): Dataset for evaluation. optimizers (`tuple` of `torch.optim.Optimizer` and `torch.optim.lr_scheduler.LambdaLR`, *optional*, defaults to `(None, None)`): Tuple containing the optimizer and the learning rate scheduler to use for training. If `None`, the optimizer and the learning rate scheduler are created using the [`~transformers.Trainer.create_optimizer_and_scheduler`] method. callbacks (`list` of [`~transformers.TrainerCallback`], *optional*): Callbacks to use during training. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration to use PEFT for training. If `None`, PEFT is not used. If provided, the policy `model` will be wrapped with the specified PEFT adapter. """ _tag_names = ["trl", "ppo"] _name = "PPO" _paper = { "title": "Fine-Tuning Language Models from Human Preferences", "id": "1909.08593", # docstyle-ignore "citation": textwrap.dedent("""\ @article{mziegler2019fine-tuning, title = {{Fine-Tuning Language Models from Human Preferences}}, author = {Daniel M. Ziegler and Nisan Stiennon and Jeffrey Wu and Tom B. Brown and Alec Radford and Dario Amodei and Paul F. Christiano and Geoffrey Irving}, year = 2019, eprint = {arXiv:1909.08593} }"""), } def __init__( self, args: PPOConfig, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin, model: nn.Module, ref_model: nn.Module | None, reward_model: nn.Module, train_dataset: Dataset, value_model: nn.Module, data_collator: DataCollatorWithPadding | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, # less commonly used optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), callbacks: list[TrainerCallback] | None = None, peft_config: "PeftConfig | None" = None, ) -> None: if train_dataset is None: raise ValueError("`train_dataset` is required") if ref_model is model: raise ValueError( "`model` and `ref_model` cannot be the same object. If you want `ref_model` to be the " "same as `model`, you must make a copy of it, or `None` if you use peft." ) self.args = args self.processing_class = processing_class self.policy_model = model # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) # Define the collator if not provided if data_collator is None: data_collator = DataCollatorWithPadding(self.processing_class) # Handle stop token settings: update policy model's generation_config to use provided stop token if args.stop_token and args.stop_token_id: raise ValueError("You cannot set both `stop_token` and `stop_token_id`.") elif args.stop_token: if args.stop_token == "eos": self.policy_model.generation_config.eos_token_id = self.stop_token_id = processing_class.eos_token_id else: raise ValueError( f"Unknown `stop_token` {args.stop_token}. Allowed values are: `'eos'` and `None` (no stop token)." ) else: self.policy_model.generation_config.eos_token_id = self.stop_token_id = args.stop_token_id # None or int # Check that the kl estimator is valid if self.args.kl_estimator not in {"k1", "k3"}: raise ValueError( "kl_estimator must be either 'k1' (straightforward, unbiased) or 'k3' (lower variance, unbiased, " "appears to be a strictly better estimator). See " "[Approximating KL Divergence](http://joschu.net/blog/kl-approx.html) for details." ) # peft support if not is_peft_available() and peft_config is not None: raise ImportError( "PEFT is not installed and you passed a `peft_config` in the trainer's kwargs, please install it to use the PEFT models" ) elif is_peft_available() and peft_config is not None: if isinstance(self.policy_model, PeftModel): raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first " "merge and unload the existing adapter, save the resulting base model, and then pass that base " "model along with the new `peft_config` to the trainer." ) # get peft model with the given config self.policy_model = get_peft_model(self.policy_model, peft_config) if args.bf16 and getattr(self.policy_model, "is_loaded_in_4bit", False): peft_module_casting_to_bf16(self.policy_model) self.is_peft_model = is_peft_available() and isinstance(self.policy_model, PeftModel) self.model_adapter_name = args.model_adapter_name self.ref_adapter_name = args.ref_adapter_name if ref_model: self.ref_model = ref_model elif self.is_peft_model: self.ref_model = None else: self.ref_model = create_reference_model(self.policy_model) self.reward_model = reward_model self.train_dataset = train_dataset self.train_dataset_len = len(train_dataset) self.value_model = value_model self.data_collator = data_collator self.eval_dataset = eval_dataset self.optimizer, self.lr_scheduler = optimizers self.optimizer_cls_and_kwargs = None # needed for transformers >= 4.47 ######### # calculate various batch sizes ######### if args.total_episodes is None: # allow the users to define episodes in terms of epochs. args.total_episodes = int(args.num_train_epochs * self.train_dataset_len) accelerator = Accelerator(gradient_accumulation_steps=args.gradient_accumulation_steps) self.accelerator = accelerator args.world_size = accelerator.num_processes args.local_batch_size = args.per_device_train_batch_size * args.gradient_accumulation_steps args.micro_batch_size = int(args.per_device_train_batch_size * args.world_size) args.batch_size = int(args.local_batch_size * args.world_size) args.mini_batch_size = exact_div( args.batch_size, args.num_mini_batches, "`batch_size` must be a multiple of `num_mini_batches`" ) args.local_mini_batch_size = exact_div( args.local_batch_size, args.num_mini_batches, "`local_batch_size` must be a multiple of `num_mini_batches`" ) if args.whiten_rewards: assert args.local_mini_batch_size >= 8, ( f"Per-rank minibatch size {args.local_mini_batch_size} is insufficient for whitening" ) # `per_rank_rollout_batch_size` is our `args.local_batch_size` # `per_rank_minibatch_size` is our `args.local_mini_batch_size` args.num_total_batches = math.ceil( args.total_episodes / args.batch_size ) # we may train for more than `total_episodes` self.local_seed = args.seed + accelerator.process_index * 100003 # Prime if args.num_sample_generations > 0: self.sample_generations_freq = max(1, args.num_total_batches // args.num_sample_generations) self.local_dataloader_batch_size = args.local_batch_size ######### # setup model, optimizer, and others ######### for module in [self.policy_model, self.ref_model, self.value_model, self.reward_model]: if module is not None: disable_dropout_in_model(module) self.model = PolicyAndValueWrapper(self.policy_model, self.value_model) self.model.config = self.policy_model.config # needed for pushing to hub self.create_optimizer_and_scheduler( num_training_steps=args.num_total_batches ) # note that we are calling `self.lr_scheduler.step()` manually only at the batch level ######### # trainer specifics ######### default_callbacks = DEFAULT_CALLBACKS + get_reporting_integration_callbacks(self.args.report_to) self.callbacks = default_callbacks if callbacks is None else default_callbacks + callbacks self.callback_handler = CallbackHandler( self.callbacks, self.model, self.processing_class, self.optimizer, self.lr_scheduler ) self.add_callback(PrinterCallback if self.args.disable_tqdm else DEFAULT_PROGRESS_CALLBACK) self.control = TrainerControl() self.state = OnlineTrainerState( is_local_process_zero=self.is_local_process_zero(), is_world_process_zero=self.is_world_process_zero(), stateful_callbacks=[ cb for cb in self.callback_handler.callbacks + [self.control] if isinstance(cb, ExportableState) ], ) self.current_flos = 0 self.hp_search_backend = None self.is_deepspeed_enabled = getattr(self.accelerator.state, "deepspeed_plugin", None) is not None self.is_fsdp_enabled = getattr(self.accelerator.state, "fsdp_plugin", None) is not None # Create distant repo and output directory if needed self.hub_model_id = None if self.args.push_to_hub: self.init_hf_repo() if self.args.should_save: os.makedirs(self.args.output_dir, exist_ok=True) # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) ######### # setup dataloader ######### self.dataloader = DataLoader( self.train_dataset, batch_size=self.local_dataloader_batch_size, shuffle=True, collate_fn=self.data_collator, drop_last=True, # needed; otherwise the last batch will be of ragged shape ) # sync random states for DataLoader(shuffle=True) before `accelerator.prepare` # see https://gist.github.com/vwxyzjn/2581bff1e48e185e0b85b6dfe1def79c torch.manual_seed(args.seed) self.model, self.optimizer, self.dataloader = accelerator.prepare(self.model, self.optimizer, self.dataloader) torch.manual_seed(self.local_seed) # reset the local seed again self.eval_dataloader = DataLoader( self.eval_dataset, batch_size=args.per_device_eval_batch_size, collate_fn=self.data_collator, drop_last=True, ) # no need to shuffle eval dataset self.eval_dataloader = accelerator.prepare(self.eval_dataloader) if self.is_deepspeed_enabled: self.reward_model = prepare_deepspeed( self.reward_model, args.per_device_train_batch_size, args.fp16, args.bf16 ) if self.ref_model is None: if not self.is_peft_model: raise ValueError("No reference model and model is not a Peft model.") else: self.ref_model = prepare_deepspeed( self.ref_model, args.per_device_train_batch_size, args.fp16, args.bf16 ) else: if self.ref_model is None: if not self.is_peft_model: raise ValueError("No reference model and model is not a Peft model.") else: self.ref_model = self.ref_model.to(self.accelerator.device) self.reward_model = self.reward_model.to(self.accelerator.device) def get_train_dataloader(self) -> DataLoader: return self.dataloader def get_eval_dataloader(self) -> DataLoader: return self.eval_dataloader @contextmanager def null_ref_context(self): """Context manager for handling null reference model (that is, peft adapter manipulation).""" with ( self.accelerator.unwrap_model(self.model.policy).disable_adapter() if self.is_peft_model and not self.ref_adapter_name else nullcontext() ): if self.ref_adapter_name: self.model.policy.set_adapter(self.ref_adapter_name) yield if self.ref_adapter_name: self.model.policy.set_adapter(self.model_adapter_name or "default") def save_model(self, output_dir: str | None = None, _internal_call: bool = False): backup_model = self.model if hasattr(self.model, "policy"): self.model = self.model.policy # save only the policy for inference if self.is_deepspeed_enabled: backup_deepspeed = self.deepspeed self.deepspeed = self.model super().save_model(output_dir, _internal_call) self.model = backup_model if self.is_deepspeed_enabled: self.deepspeed = backup_deepspeed def train(self): args = self.args accelerator = self.accelerator optimizer = self.optimizer model = self.model ref_policy = self.ref_model reward_model = self.reward_model processing_class = self.processing_class dataloader = self.dataloader device = accelerator.device def repeat_generator(): while True: yield from dataloader iter_dataloader = iter(repeat_generator()) generation_kwargs = { "max_new_tokens": args.response_length, "temperature": (args.temperature + 1e-7), "top_k": 0.0, "top_p": 1.0, "do_sample": True, } generation_config = GenerationConfig(**generation_kwargs) accelerator.print("===training policy===") start_time = time.time() stats_shape = (args.num_ppo_epochs, args.num_mini_batches, args.gradient_accumulation_steps) approxkl_stats = torch.zeros(stats_shape, device=device) pg_clipfrac_stats = torch.zeros(stats_shape, device=device) pg_loss_stats = torch.zeros(stats_shape, device=device) vf_loss_stats = torch.zeros(stats_shape, device=device) vf_clipfrac_stats = torch.zeros(stats_shape, device=device) entropy_stats = torch.zeros(stats_shape, device=device) ratio_stats = torch.zeros(stats_shape, device=device) model.train() # trainer state initialization self.state.global_step = 0 self.state.episode = 0 self.state.max_steps = args.num_total_batches self.state.num_train_epochs = args.total_episodes / self.train_dataset_len # Compute absolute values for logging, eval, and save if given as ratio if args.logging_steps is not None: if args.logging_steps < 1: self.state.logging_steps = math.ceil(self.state.max_steps * args.logging_steps) else: self.state.logging_steps = args.logging_steps if args.eval_steps is not None: if args.eval_steps < 1: self.state.eval_steps = math.ceil(self.state.max_steps * args.eval_steps) else: self.state.eval_steps = args.eval_steps if args.save_steps is not None: if args.save_steps < 1: self.state.save_steps = math.ceil(self.state.max_steps * args.save_steps) else: self.state.save_steps = args.save_steps self.control = self.callback_handler.on_train_begin(args, self.state, self.control) # backward compatibility if self.is_deepspeed_enabled: self.deepspeed = self.model self.model_wrapped = self.model for update in range(1, args.num_total_batches + 1): self.state.episode += 1 * args.batch_size data = next(iter_dataloader) with torch.no_grad(): queries = data["input_ids"].to(device) context_length = queries.shape[1] responses = [] postprocessed_responses = [] logprobs = [] ref_logprobs = [] scores = [] sequence_lengths = [] values = [] with ( unwrap_model_for_generation( self.model, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model ): query_responses, logitss = batch_generation( unwrapped_model.policy, queries, args.local_rollout_forward_batch_size, processing_class.pad_token_id, generation_config, ) for i in range(0, queries.shape[0], args.local_rollout_forward_batch_size): query = queries[i : i + args.local_rollout_forward_batch_size] query_response = query_responses[i : i + args.local_rollout_forward_batch_size] response = query_response[:, context_length:] logits = logitss[i : i + args.local_rollout_forward_batch_size] logprob = selective_log_softmax(logits, response) del logits empty_cache() if ref_policy is None: with self.null_ref_context(): ref_output = forward(model.policy, query_response, processing_class.pad_token_id) else: ref_output = forward(ref_policy, query_response, processing_class.pad_token_id) ref_logits = ref_output.logits[:, context_length - 1 : -1] ref_logits /= args.temperature + 1e-7 ref_logprob = selective_log_softmax(ref_logits, response) del ref_output, ref_logits empty_cache() # Response Processing 1. truncate response after the first occurrence of `stop_token_id` postprocessed_response = response if self.stop_token_id is not None: # handle the edge case when stop_token_id exists but is 0 postprocessed_response = truncate_response( self.stop_token_id, processing_class.pad_token_id, response ) # Response Processing 2. run reward model on the truncated responses postprocessed_query_response = torch.cat((query, postprocessed_response), 1) sequence_length = first_true_indices(postprocessed_response == processing_class.pad_token_id) - 1 unwrapped_value_model = accelerator.unwrap_model(model).value_model full_value, _, _ = get_reward( unwrapped_value_model, query_response, processing_class.pad_token_id, context_length ) value = full_value[:, context_length - 1 : -1].squeeze(-1) _, score, _ = get_reward( reward_model, postprocessed_query_response, processing_class.pad_token_id, context_length ) responses.append(response) postprocessed_responses.append(postprocessed_response) logprobs.append(logprob) ref_logprobs.append(ref_logprob) sequence_lengths.append(sequence_length) scores.append(score) values.append(value) responses = torch.cat(responses, 0) postprocessed_responses = torch.cat(postprocessed_responses, 0) logprobs = torch.cat(logprobs, 0) ref_logprobs = torch.cat(ref_logprobs, 0) sequence_lengths = torch.cat(sequence_lengths, 0) scores = torch.cat(scores, 0) values = torch.cat(values, 0) del (logprob, ref_logprob, full_value, value, score, unwrapped_model) empty_cache() gc.collect() # Response Processing 3. Filter completion. Ensure that the sample contains stop_token_id # Completions not passing that filter will receive a lower score. contain_eos_token = torch.any(postprocessed_responses == self.processing_class.eos_token_id, dim=-1) if self.args.missing_eos_penalty is not None: scores[~contain_eos_token] -= self.args.missing_eos_penalty # accelerator.print(f"{scores=}, {(contain_eos_token.sum() / len(contain_eos_token))=}") # be very careful with `padding_mask_p1`; see https://excalidraw.com/#json=LWnzG4w2k5DjF_EOL_xPt,e2w3a-hFJ_gX5vOfeyXGTw response_idxs = torch.arange(responses.shape[1], device=responses.device).repeat(responses.shape[0], 1) padding_mask = response_idxs > sequence_lengths.unsqueeze(1) logprobs = torch.masked_fill(logprobs, padding_mask, INVALID_LOGPROB) ref_logprobs = torch.masked_fill(ref_logprobs, padding_mask, INVALID_LOGPROB) sequence_lengths_p1 = sequence_lengths + 1 padding_mask_p1 = response_idxs > (sequence_lengths_p1.unsqueeze(1)) values = torch.masked_fill(values, padding_mask_p1, 0) # 4. compute rewards # Formula used by http://joschu.net/blog/kl-approx.html for the k1 and k3 estimators logr = ref_logprobs - logprobs kl = -logr if args.kl_estimator == "k1" else (logr.exp() - 1) - logr # Else statement is k3 non_score_reward = -args.kl_coef * kl rewards = non_score_reward.clone() actual_start = torch.arange(rewards.size(0), device=rewards.device) actual_end = torch.where(sequence_lengths_p1 < rewards.size(1), sequence_lengths_p1, sequence_lengths) rewards[actual_start, actual_end] += scores # 5. whiten rewards if args.whiten_rewards: rewards = masked_whiten(rewards, mask=~padding_mask_p1, shift_mean=False) rewards = torch.masked_fill(rewards, padding_mask_p1, 0) # 6. compute advantages and returns lastgaelam = 0 advantages_reversed = [] gen_length = responses.shape[1] for t in reversed(range(gen_length)): nextvalues = values[:, t + 1] if t < gen_length - 1 else 0.0 delta = rewards[:, t] + args.gamma * nextvalues - values[:, t] lastgaelam = delta + args.gamma * args.lam * lastgaelam advantages_reversed.append(lastgaelam) advantages = torch.stack(advantages_reversed[::-1], axis=1) returns = advantages + values advantages = masked_whiten(advantages, ~padding_mask) advantages = torch.masked_fill(advantages, padding_mask, 0) empty_cache() # Do multiple epochs of PPO training, with a fresh random shuffle in each epoch for ppo_epoch_idx in range(args.num_ppo_epochs): b_inds = np.random.permutation(args.local_batch_size) minibatch_idx = 0 for mini_batch_start in range(0, args.local_batch_size, args.local_mini_batch_size): mini_batch_end = mini_batch_start + args.local_mini_batch_size mini_batch_inds = b_inds[mini_batch_start:mini_batch_end] gradient_accumulation_idx = 0 for micro_batch_start in range(0, args.local_mini_batch_size, args.per_device_train_batch_size): with accelerator.accumulate(model): micro_batch_end = micro_batch_start + args.per_device_train_batch_size micro_batch_inds = mini_batch_inds[micro_batch_start:micro_batch_end] mb_advantage = advantages[micro_batch_inds] mb_responses = responses[micro_batch_inds] mb_query_responses = query_responses[micro_batch_inds] mb_logprobs = logprobs[micro_batch_inds] mb_return = returns[micro_batch_inds] mb_values = values[micro_batch_inds] output, vpred_temp = forward(model, mb_query_responses, processing_class.pad_token_id) logits = output.logits[:, context_length - 1 : -1] logits /= args.temperature + 1e-7 new_logprobs = selective_log_softmax(logits, mb_responses) new_logprobs = torch.masked_fill( new_logprobs, padding_mask[micro_batch_inds], INVALID_LOGPROB ) vpred = vpred_temp[:, context_length - 1 : -1].squeeze(-1) vpred = torch.masked_fill(vpred, padding_mask_p1[micro_batch_inds], 0) vpredclipped = torch.clamp( vpred, mb_values - args.cliprange_value, mb_values + args.cliprange_value, ) vf_losses1 = torch.square(vpred - mb_return) vf_losses2 = torch.square(vpredclipped - mb_return) vf_loss_max = torch.max(vf_losses1, vf_losses2) vf_loss = 0.5 * masked_mean(vf_loss_max, ~padding_mask_p1[micro_batch_inds]) vf_clipfrac = masked_mean( (vf_losses2 > vf_losses1).float(), ~padding_mask_p1[micro_batch_inds] ) logprobs_diff = new_logprobs - mb_logprobs ratio = torch.exp(logprobs_diff) pg_losses = -mb_advantage * ratio pg_losses2 = -mb_advantage * torch.clamp(ratio, 1.0 - args.cliprange, 1.0 + args.cliprange) pg_loss_max = torch.max(pg_losses, pg_losses2) pg_loss = masked_mean(pg_loss_max, ~padding_mask[micro_batch_inds]) loss = pg_loss + args.vf_coef * vf_loss accelerator.backward(loss) optimizer.step() optimizer.zero_grad() with torch.no_grad(): pg_clipfrac = masked_mean( (pg_losses2 > pg_losses).float(), ~padding_mask[micro_batch_inds] ) prob_dist = torch.nn.functional.softmax(logits, dim=-1) entropy = torch.logsumexp(logits, dim=-1) - torch.sum(prob_dist * logits, dim=-1) approxkl = 0.5 * (logprobs_diff**2).mean() approxkl_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = approxkl pg_clipfrac_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = ( pg_clipfrac ) pg_loss_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = pg_loss vf_loss_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = vf_loss vf_clipfrac_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = ( vf_clipfrac ) entropy_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = entropy.mean() ratio_stats[ppo_epoch_idx, minibatch_idx, gradient_accumulation_idx] = ratio.mean() gradient_accumulation_idx += 1 minibatch_idx += 1 # del everything and empty cache # fmt: off del ( output, vpred_temp, logits, new_logprobs, vpred, vpredclipped, vf_losses1, vf_losses2, vf_loss, vf_clipfrac, logprobs_diff, ratio, pg_losses, pg_losses2, pg_loss_max, pg_loss, loss, pg_clipfrac, prob_dist, entropy, approxkl, mb_return, mb_advantage, mb_values, mb_responses, mb_query_responses, mb_logprobs, ) # fmt: on empty_cache() with torch.no_grad(): mean_kl = kl.sum(1).mean() mean_entropy = (-logprobs).sum(1).mean() mean_non_score_reward = non_score_reward.sum(1).mean() rlhf_reward = mean_non_score_reward + scores.mean() eps = int(self.state.episode / (time.time() - start_time)) metrics = {} metrics["eps"] = eps metrics["objective/kl"] = self.accelerator.gather_for_metrics(mean_kl).mean().item() metrics["objective/entropy"] = self.accelerator.gather_for_metrics(mean_entropy).mean().item() metrics["objective/non_score_reward"] = ( self.accelerator.gather_for_metrics(mean_non_score_reward).mean().item() ) metrics["objective/rlhf_reward"] = self.accelerator.gather_for_metrics(rlhf_reward).mean().item() metrics["objective/scores"] = self.accelerator.gather_for_metrics(scores.mean()).mean().item() metrics["policy/approxkl_avg"] = self.accelerator.gather_for_metrics(approxkl_stats).mean().item() metrics["policy/clipfrac_avg"] = self.accelerator.gather_for_metrics(pg_clipfrac_stats).mean().item() metrics["loss/policy_avg"] = self.accelerator.gather_for_metrics(pg_loss_stats).mean().item() metrics["loss/value_avg"] = self.accelerator.gather_for_metrics(vf_loss_stats).mean().item() metrics["val/clipfrac_avg"] = self.accelerator.gather_for_metrics(vf_clipfrac_stats).mean().item() metrics["policy/entropy_avg"] = self.accelerator.gather_for_metrics(entropy_stats).mean().item() metrics["val/ratio"] = self.accelerator.gather_for_metrics(ratio_stats).mean().item() metrics["val/ratio_var"] = self.accelerator.gather_for_metrics(ratio_stats).var().item() metrics["val/num_eos_tokens"] = (responses == processing_class.eos_token_id).sum().item() metrics["lr"] = self.lr_scheduler.get_last_lr()[0] metrics["episode"] = self.state.episode self.state.epoch = self.state.episode / self.train_dataset_len # used by self.log self.state.global_step += 1 self.log(metrics) self.lr_scheduler.step() self.control = self.callback_handler.on_step_end(args, self.state, self.control) if self.control.should_save: self._save_checkpoint(model, trial=None) self.control = self.callback_handler.on_save(self.args, self.state, self.control) del kl, mean_kl, mean_entropy, mean_non_score_reward, scores, metrics, non_score_reward empty_cache() gc.collect() if args.num_sample_generations > 0 and (update - 1) % self.sample_generations_freq == 0: self.generate_completions(sampling=True) empty_cache() del ( query_responses, responses, postprocessed_responses, logprobs, ref_logprobs, values, sequence_lengths, contain_eos_token, sequence_lengths_p1, response_idxs, padding_mask, padding_mask_p1, rewards, actual_start, actual_end, advantages, returns, ) empty_cache() # HF trainer specifics self.control = self.callback_handler.on_train_end(args, self.state, self.control) if self.control.should_save: self._save_checkpoint(model, trial=None) self.control = self.callback_handler.on_save(self.args, self.state, self.control) def generate_completions(self, sampling: bool = False): if self.eval_dataset is None: return # no eval set to sample from (pass eval_dataset and eval_strategy != "no" for sample generations) args = self.args processing_class = self.processing_class generation_kwargs = { "max_new_tokens": args.response_length, "temperature": (0.01 + 1e-7), "top_k": 0.0, "top_p": 1.0, "do_sample": True, } generation_config = GenerationConfig(**generation_kwargs) table = defaultdict(list) with ( unwrap_model_for_generation( self.model, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model ): for batch in self.eval_dataloader: query = batch["input_ids"] with torch.no_grad(): context_length = query.shape[1] query_response, _ = batch_generation( unwrapped_model.policy, query, query.shape[0], processing_class.pad_token_id, generation_config, ) response = query_response[:, context_length:] postprocessed_response = response if self.stop_token_id is not None: # handle the edge case when stop_token_id exists but is 0 postprocessed_response = truncate_response( self.stop_token_id, processing_class.pad_token_id, response ) table["query"].extend( gather_object(processing_class.batch_decode(query, skip_special_tokens=True)) ) table["model response"].extend( gather_object(processing_class.batch_decode(postprocessed_response)) ) postprocessed_query_response = torch.cat((query, postprocessed_response), 1) _, score, _ = get_reward( self.reward_model, postprocessed_query_response, processing_class.pad_token_id, context_length ) table["score"].extend(self.accelerator.gather_for_metrics(score).float().cpu().numpy()) if sampling: break df = pd.DataFrame(table) if self.accelerator.is_main_process: if is_rich_available(): print_rich_table(df.iloc[0 : 0 + 5]) if "wandb" in args.report_to: import wandb if wandb.run is not None: wandb.log({"completions": wandb.Table(dataframe=df)}) if "comet_ml" in args.report_to: log_table_to_comet_experiment( name="completions.csv", table=df, ) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/prm/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .prm_config import PRMConfig from .prm_trainer import PRMTrainer __all__ = ["PRMConfig", "PRMTrainer"] ================================================ FILE: trl/experimental/prm/prm_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from ...trainer.base_config import _BaseConfig @dataclass class PRMConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`experimental.prm.PRMTrainer`]. This class includes only the parameters that are specific to PRM training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the sequences (prompt + completion) used for truncation. max_completion_length (`int`, *optional*): Maximum length of the completion used for truncation. The completion is the concatenation of the steps. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. step_separator (`str`, *optional*, defaults to `"\n"`): Separator used to separate each step of the reasoning process. train_on_last_step_only (`bool`, *optional*, defaults to `False`): Whether to train only on the last step. dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-5` instead of `5e-5`. """ # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-5, metadata={"help": "The initial learning rate for AdamW."}, ) max_length: int | None = field( default=1024, metadata={"help": "Maximum length of the sequences (prompt + completion) used for truncation."}, ) max_completion_length: int | None = field( default=None, metadata={ "help": "Maximum length of the completion used for truncation. The completion is the concatenation of the " "steps." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model and reference model."}, ) step_separator: str = field( default="\n", metadata={"help": "Separator used to separate each step of the reasoning process."}, ) train_on_last_step_only: bool = field( default=False, metadata={"help": "Whether to train only on the last step."}, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) ================================================ FILE: trl/experimental/prm/prm_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap from collections.abc import Callable from itertools import chain from pathlib import Path import numpy as np import torch import torch.nn as nn import transformers from accelerate import PartialState, logging from datasets import Dataset, features from packaging.version import Version from transformers import ( BaseImageProcessor, DataCollator, DataCollatorForTokenClassification, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, ) from transformers.trainer_utils import EvalPrediction from transformers.utils import is_peft_available from ...trainer.base_trainer import _BaseTrainer from ...trainer.utils import disable_dropout_in_model from ..utils import prepare_peft_model from .prm_config import PRMConfig if is_peft_available(): from peft import PeftModel logger = logging.get_logger(__name__) def compute_accuracy(eval_pred: EvalPrediction) -> dict[str, float]: predictions, labels = eval_pred if predictions.ndim == 3: # Token classification task. Shapes are (batch_size, seq_len, num_labels) and (batch_size, seq_len) # Used to compute the accuracy in the prm_trainer. predictions = np.argmax(predictions, axis=2) # Flatten the predictions and labels to remove the ignored tokens. predictions = np.array( [ p for prediction, label in zip(predictions, labels, strict=True) for (p, lbl) in zip(prediction, label, strict=True) if lbl != -100 ] ) labels = np.array([lbl for label in labels for lbl in label if lbl != -100]) else: # Here, predictions is rewards_chosen and rewards_rejected. Shapes are (batch_size, 2) and (batch_size,) # We want to see how much of the time rewards_chosen > rewards_rejected. equal_mask = predictions[:, 0] == predictions[:, 1] equal_predictions_count = int(equal_mask.sum()) if equal_predictions_count > 0: # Before using the logger, the accelerate state must be initialized. It'susually the case when using this # function inside a Trainer, but it may not be the case otherwise, in particular when unit testing. PartialState() logger.warning( f"There are {equal_predictions_count} out of {len(predictions[:, 0])} instances where the predictions " "for both options are equal. These instances are ignored in the accuracy computation.", ) # Filter out equal predictions predictions = predictions[~equal_mask] labels = labels[~equal_mask] # Use the remaining predictions for accuracy calculation predictions = np.argmax(predictions, axis=1) accuracy = np.array(predictions == labels, dtype=float).mean().item() return {"accuracy": accuracy} class PRMTrainer(_BaseTrainer): """ Initialize PRMTrainer. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an `AutoModelForTokenClassification`. args ([`experimental.prm.PRMConfig`]): The arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`~transformers.DataCollatorForTokenClassification`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. model_init (`Callable[[], transformers.PreTrainedModel]`): The model initializer to use for training. If None is specified, the default model initializer will be used. compute_metrics (`Callable[[transformers.EvalPrediction], dict]`, *optional* defaults to `compute_accuracy`): The metrics to use for evaluation. If no metrics are specified, the default metric (`compute_accuracy`) will be used. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. peft_config (`dict`, defaults to `None`): The PEFT configuration to use for training. If you pass a PEFT configuration, the model will be wrapped in a PEFT model. """ _tag_names = ["trl", "prm"] _name = "PRM" _paper = { "title": "Solving math word problems with process-and outcome-based feedback", "id": "2211.14275", # docstyle-ignore "citation": textwrap.dedent("""\ @article{uesato2022solving, title = {{Solving Math Word Problems With Process- and Outcome-Based Feedback}}, author = {Uesato, Jonathan and Kushman, Nate and Kumar, Ramana and Song, Francis and Siegel, Noah and Wang, Lisa and Creswell, Antonia and Irving, Geoffrey and Higgins, Irina}, year = 2022, journal = {arXiv preprint arXiv:2211.14275} }"""), } def __init__( self, model: PreTrainedModel | nn.Module | None = None, args: PRMConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, model_init: Callable[[], PreTrainedModel] | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = ( None, None, ), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: dict | None = None, ): if train_dataset is None: raise ValueError("`train_dataset` is required") if peft_config is not None or (is_peft_available() and isinstance(model, PeftModel)): model = prepare_peft_model(model, peft_config, args) # Disable dropout in the model if args.disable_dropout: disable_dropout_in_model(model) if compute_metrics is None: compute_metrics = compute_accuracy if data_collator is None: if processing_class is None: raise ValueError( "A processing_class must be specified when using the default DataCollatorForTokenClassification" ) data_collator = DataCollatorForTokenClassification(processing_class) if "input_ids" not in train_dataset.column_names: with PartialState().main_process_first(): fn_kwargs = { "tokenizer": processing_class, "step_separator": args.step_separator, "max_length": args.max_length, "max_completion_length": args.max_completion_length, "train_on_last_step_only": args.train_on_last_step_only, } train_fn_kwargs = {**fn_kwargs, "is_eval": False} train_dataset = train_dataset.map( self.tokenize_row, fn_kwargs=train_fn_kwargs, num_proc=args.dataset_num_proc, remove_columns=train_dataset.features, desc="Tokenizing train dataset", features=features.Features( # needed to avoid map to cast labels to bool { "labels": features.Sequence(features.Value("int64")), "input_ids": features.Sequence(features.Value("int64")), } ), ) eval_fn_kwargs = {**fn_kwargs, "is_eval": True} if eval_dataset is not None: eval_dataset = eval_dataset.map( self.tokenize_row, fn_kwargs=eval_fn_kwargs, num_proc=args.dataset_num_proc, remove_columns=eval_dataset.features, desc="Tokenizing eval dataset", features=features.Features( # needed to avoid map to cast labels to bool { "labels": features.Sequence(features.Value("int64")), "input_ids": features.Sequence(features.Value("int64")), } ), ) # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, model_init=model_init, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Add tags for models that have been loaded with the correct transformers version if hasattr(self.model, "add_model_tags"): self.model.add_model_tags(self._tag_names) @staticmethod def tokenize_row( features, tokenizer, step_separator, max_length, max_completion_length, train_on_last_step_only, is_eval, ): r""" Tokenize a row of the dataset. Args: features (`dict[str, str]`): Row of the dataset, should contain the keys `"prompt"`, `"completions"`, and `"labels"`. tokenizer ([`~transformers.PreTrainedTokenizerBase`]): Tokenizer used to process the data. step_separator (`str`): Separator between steps in the completion. max_length (`int` or `None`): Maximum length of the sequences (prompt + completion). If `None`, the sequences are not truncated. max_completion_length (`int` or `None`): Maximum length of the completion sequences. If `None`, the completion sequences are not truncated. train_on_last_step_only (`bool`): Whether to train only on the last step. If `True`, the labels are `-100` for all tokens except the last token of the completion. is_eval (`bool`): Whether the function is used to tokenize samples from a training or an evaluation dataset. Used only if `train_on_last_step_only` is set to `True`. Returns: `dict[str, list[int]]`: Tokenized sequences with the keys `"input_ids"`, and `"labels". Example: ```python >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B") >>> features = { ... "prompt": "Which number is larger, 9.8 or 9.11?", ... "completions": ["11 is greater than 8.", "Hence, 9.11 > 9.8."], ... "labels": [True, False], ... } >>> PRMTrainer.tokenize_row( ... features, tokenizer, "\n", max_completion_length=None, train_on_last_step_only=False, is_eval=False ... ) {'input_ids': [23085, 1372, 374, 8131, 11, 220, 24, 13, 23, 476, 220, 24, 13, 16, 16, 30, 16, 16, 374, 7046, 1091, 220, 23, 13, 198, 39, 763, 11, 220, 24, 13, 16, 16, 861, 220, 24, 13, 23, 13, 198], 'labels': [-100, -100, -100, -100, -100, -100, -100, -100, 1, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 0]} ``` """ # Tokenize the prompt and completions prompt_ids = tokenizer(features["prompt"], add_special_tokens=False)["input_ids"] completions_ids = [ tokenizer(completion, add_special_tokens=False)["input_ids"] for completion in features["completions"] ] if train_on_last_step_only and not is_eval: labels = [-100] * (len(features["labels"]) - 1) + [int(features["labels"][-1])] else: labels = [int(label) for label in features["labels"]] # Get the ID of the separator token and add it to the completions separator_ids = tokenizer.encode(step_separator, add_special_tokens=False) completions_ids = [completion + separator_ids for completion in completions_ids] # Create the label labels = [ [-100] * (len(completion) - 1) + [label] for completion, label in zip(completions_ids, labels, strict=True) ] # Join the completions and labels steps completion_ids = list(chain(*completions_ids)) labels = list(chain(*labels)) if tokenizer.bos_token_id is not None: prompt_ids = [tokenizer.bos_token_id] + prompt_ids # Truncate completion sequences if max_completion_length is not None: completion_ids = completion_ids[:max_completion_length] labels = labels[:max_completion_length] input_ids = prompt_ids + completion_ids labels = [-100] * len(prompt_ids) + labels if max_length is not None: input_ids = input_ids[:max_length] labels = labels[:max_length] return {"input_ids": input_ids, "labels": labels} # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/experimental/utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This file contains utility classes and functions that are used across more than one experimental trainer or feature. import inspect import logging from copy import deepcopy from dataclasses import dataclass from typing import Any import torch from accelerate.utils import is_peft_model from packaging.version import Version from torch import nn from torch.nn.utils.rnn import pad_sequence from transformers import PreTrainedModel, PreTrainedTokenizerBase, TrainingArguments from transformers.integrations.deepspeed import is_deepspeed_zero3_enabled from transformers.utils import ( is_peft_available, is_torch_mlu_available, is_torch_npu_available, is_torch_xpu_available, ) from ..trainer.utils import pad if is_peft_available(): import peft from peft import PeftConfig, PeftModel, get_peft_model @dataclass class DPODataCollatorWithPadding: r""" DPO DataCollator class that pads the tokenized inputs to the maximum length of the batch. Args: pad_token_id (`int` defaults to 0): The tokenizer's pad_token_id. is_encoder_decoder (`bool` or `None`, `optional`, defaults to `None`): Whether you model has an encoder_decoder architecture. """ pad_token_id: int = 0 is_encoder_decoder: bool | None = False def __call__(self, features: list[dict[str, Any]]) -> dict[str, Any]: # first, pad everything to the same length padded_batch = {} for k in features[0].keys(): if k.endswith(("_input_ids", "_attention_mask", "_labels", "_pixel_values")): if self.is_encoder_decoder: to_pad = [torch.LongTensor(ex[k]) for ex in features] if (k.startswith("prompt")) and (k.endswith("input_ids")): if self.pad_token_id is None: raise ValueError( "Padding is enabled, but the tokenizer is not configured with a padding token." " Explicitly set `tokenizer.pad_token` (e.g. `tokenizer.pad_token = tokenizer.eos_token`)" " before calling the trainer." ) padding_value = self.pad_token_id elif k.endswith("_attention_mask"): padding_value = 0 elif k.startswith(("chosen", "rejected", "completion")) or ("decoder" in k): padding_value = -100 else: raise ValueError(f"Unexpected key in batch '{k}'") padded_batch[k] = pad_sequence(to_pad, batch_first=True, padding_value=padding_value) else: # Set padding value based on the key if k.endswith("_input_ids"): if self.pad_token_id is None: raise ValueError( "Padding is enabled, but the tokenizer is not configured with a padding token." " Explicitly set `tokenizer.pad_token` (e.g. `tokenizer.pad_token = tokenizer.eos_token`)" " before calling the trainer." ) padding_value = self.pad_token_id elif k.endswith("_labels"): padding_value = -100 elif k.endswith("_attention_mask"): padding_value = 0 elif k.endswith("_pixel_values"): padding_value = 0 # TODO: check if this is correct else: raise ValueError(f"Unexpected key in batch '{k}'") # Set padding side based on the key if k in ["prompt_input_ids", "prompt_attention_mask"]: padding_side = "left" else: padding_side = "right" # Set the dtype if k.endswith("_pixel_values"): dtype = torch.float32 # will be downcasted if necessary by the Trainer else: dtype = torch.int64 # Convert to tensor and pad to_pad = [torch.tensor(ex[k], dtype=dtype) for ex in features] padded_batch[k] = pad(to_pad, padding_value=padding_value, padding_side=padding_side) elif k.endswith("_logps"): # the cached reference model logprobs padded_batch[k] = torch.tensor([ex[k] for ex in features]) else: padded_batch[k] = [ex[k] for ex in features] return padded_batch @dataclass class DataCollatorForChatML: """ Data collator for ChatML format datasets. """ tokenizer: PreTrainedTokenizerBase ignore_index: int = -100 max_length: int = None prompt_key: str = "prompt" messages_key: str = "messages" def __post_init__(self): if self.tokenizer.pad_token_id is None: raise ValueError("The tokenizer does not have a pad token. Please set `pad_token_id` in the tokenizer.") if self.max_length is None: # set a sensible default self.max_length = min(self.tokenizer.model_max_length, 1024) def __call__(self, examples: list[dict[str, Any]]) -> dict[str, torch.Tensor]: input_ids = [] attention_mask = [] prompts_input_ids = [] prompt_attention_mask = [] labels = [] for example in examples: formatted_prompt = example.get(self.prompt_key, None) if formatted_prompt is None: prompt = example[self.messages_key][:-1] formatted_prompt = self.tokenizer.apply_chat_template( prompt, add_generation_prompt=True, tokenize=False ) if "input_ids" not in example: message = example[self.messages_key] formatted_message = self.tokenizer.apply_chat_template( message, add_generation_prompt=False, tokenize=False ) tokenized_message = self.tokenizer( formatted_message, truncation=False, padding=False, return_tensors=None, add_special_tokens=False, return_offsets_mapping=True, ) message_input_ids_full = tokenized_message["input_ids"] offsets = tokenized_message.get("offset_mapping") if offsets is not None: prompt_char_len = len(formatted_prompt) completion_start_idx_full = next( (idx for idx, (start, _) in enumerate(offsets) if start >= prompt_char_len), len(message_input_ids_full), ) else: tokenized_prompt_full = self.tokenizer( formatted_prompt, truncation=False, padding=False, return_tensors=None, add_special_tokens=False, ) completion_start_idx_full = len(tokenized_prompt_full["input_ids"]) prompt_tokens_full = message_input_ids_full[:completion_start_idx_full] completion_input_ids_full = message_input_ids_full[completion_start_idx_full:] if self.max_length is not None and len(message_input_ids_full) > self.max_length: completion_ids = completion_input_ids_full if len(completion_ids) >= self.max_length: completion_ids = completion_ids[-self.max_length :] prompt_ids = [] else: max_prompt_tokens = self.max_length - len(completion_ids) prompt_ids = prompt_tokens_full[-max_prompt_tokens:] if max_prompt_tokens > 0 else [] message_input_ids = prompt_ids + completion_ids else: message_input_ids = message_input_ids_full prompt_ids = prompt_tokens_full input_ids.append(message_input_ids) attention_mask.append([1] * len(message_input_ids)) current_prompt_ids = prompt_ids else: message_input_ids = example["input_ids"] input_ids.append(message_input_ids) if "attention_mask" in example: attention_mask.append(example["attention_mask"]) else: attention_mask.append([1] * len(message_input_ids)) tokenized_prompt = self.tokenizer( formatted_prompt, truncation=True, max_length=len(message_input_ids), padding=False, return_tensors=None, add_special_tokens=False, ) current_prompt_ids = tokenized_prompt["input_ids"] prompts_input_ids.append(current_prompt_ids) prompt_attention_mask.append([1] * len(current_prompt_ids)) label = [self.ignore_index] * len(input_ids[-1]) completion_start_idx = len(current_prompt_ids) label[completion_start_idx:] = input_ids[-1][completion_start_idx:] labels.append(label) # convert to list of tensors and pad input_ids = [torch.tensor(ids, dtype=torch.long) for ids in input_ids] attention_mask = [torch.tensor(mask, dtype=torch.long) for mask in attention_mask] labels = [torch.tensor(label, dtype=torch.long) for label in labels] input_ids = pad(input_ids, padding_side="left", padding_value=self.tokenizer.pad_token_id) attention_mask = pad(attention_mask, padding_side="left", padding_value=0) labels = pad(labels, padding_side="left", padding_value=self.ignore_index) prompts_input_ids = [torch.tensor(ids, dtype=torch.long) for ids in prompts_input_ids] prompt_attention_mask = [torch.tensor(mask, dtype=torch.long) for mask in prompt_attention_mask] prompts_input_ids = pad(prompts_input_ids, padding_side="left", padding_value=self.tokenizer.pad_token_id) prompt_attention_mask = pad(prompt_attention_mask, padding_side="left", padding_value=0) return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels, "prompts": prompts_input_ids, "prompt_attention_mask": prompt_attention_mask, } def truncate_right( input_ids: torch.Tensor, stop_token_id: int, pad_token_id: int ) -> tuple[torch.Tensor, torch.Tensor]: """ Truncates the input tensor from the right side after the first occurrence of the stop token. Args: input_ids (`torch.Tensor`): The tensor containing the responses to be truncated stop_token_id (`int`): The token ID representing the stop token where truncation occurs pad_token_id (`int`): The token ID representing the pad token used to fill the truncated responses Returns: tuple: - `output_ids` (`torch.Tensor`): The truncated responses tensor with pad tokens filled after the stop token - `mask` (`torch.Tensor`): The mask tensor to indicate the padding tokens """ trunc_idxs = first_true_indices(input_ids == stop_token_id).unsqueeze(-1) new_size = [1] * (len(input_ids.size()) - 1) + [input_ids.shape[1]] idxs = torch.arange(input_ids.shape[1], device=input_ids.device).view(*new_size) output_ids = torch.masked_fill(input_ids, idxs > trunc_idxs, pad_token_id) mask = torch.masked_fill(torch.ones_like(input_ids), idxs > trunc_idxs, 0) return output_ids, mask SIMPLE_CHAT_TEMPLATE = "{% for message in messages %}{{message['role'].capitalize() + ': ' + message['content'] + '\n\n'}}{% endfor %}{% if add_generation_prompt %}{{ 'Assistant:' }}{% endif %}" def add_bos_token_if_needed( bos_token_id: int | None, prompt_len_input_ids: int, prompt_tokens: dict[str, list[int]], chosen_prompt_len_input_ids: int, chosen_tokens: dict[str, list[int]], rejected_prompt_len_input_ids: int, rejected_tokens: dict[str, list[int]], ): if bos_token_id is not None: if prompt_len_input_ids == 0 or bos_token_id != prompt_tokens["prompt_input_ids"][0]: prompt_tokens["prompt_input_ids"] = [bos_token_id] + prompt_tokens["prompt_input_ids"] prompt_tokens["prompt_attention_mask"] = [1] + prompt_tokens["prompt_attention_mask"] if chosen_prompt_len_input_ids == 0 or bos_token_id != chosen_tokens["prompt_input_ids"][0]: chosen_tokens["prompt_input_ids"] = [bos_token_id] + chosen_tokens["prompt_input_ids"] chosen_tokens["prompt_attention_mask"] = [1] + chosen_tokens["prompt_attention_mask"] if rejected_prompt_len_input_ids == 0 or bos_token_id != rejected_tokens["prompt_input_ids"][0]: rejected_tokens["prompt_input_ids"] = [bos_token_id] + rejected_tokens["prompt_input_ids"] rejected_tokens["prompt_attention_mask"] = [1] + rejected_tokens["prompt_attention_mask"] return prompt_tokens, chosen_tokens, rejected_tokens def add_eos_token_if_needed( eos_token_id: int, chosen_tokens: dict[str, list[int]], rejected_tokens: dict[str, list[int]] ): if len(chosen_tokens["input_ids"]) == 0 or eos_token_id != chosen_tokens["input_ids"][-1]: chosen_tokens["input_ids"].append(eos_token_id) chosen_tokens["attention_mask"].append(1) if len(rejected_tokens["input_ids"]) == 0 or eos_token_id != rejected_tokens["input_ids"][-1]: rejected_tokens["input_ids"].append(eos_token_id) rejected_tokens["attention_mask"].append(1) return chosen_tokens, rejected_tokens def first_true_indices(bools: torch.Tensor, dtype=torch.long) -> torch.Tensor: """ Takes an N-dimensional bool tensor and returns an (N-1)-dimensional tensor of integers giving the position of the first True in each "row". Returns the length of the rows (bools.size(-1)) if no element is True in a given row. Args: bools (`torch.Tensor`): An N-dimensional boolean tensor. dtype (`torch.dtype`, optional): The desired data type of the output tensor. Defaults to `torch.long`. Returns: `torch.Tensor`: An (N-1)-dimensional tensor of integers indicating the position of the first True in each row. If no True value is found in a row, returns the length of the row. """ row_len = bools.size(-1) zero_or_index = row_len * (~bools).type(dtype) + torch.arange(row_len, dtype=dtype, device=bools.device) return torch.min(zero_or_index, dim=-1).values def get_reward( model: torch.nn.Module, query_responses: torch.Tensor, pad_token_id: int, context_length: int ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """ Computes the reward logits and the rewards for a given model and query responses. Args: model (`torch.nn.Module`): The model used to compute the reward logits. query_responses (`torch.Tensor`): The tensor containing the query responses. pad_token_id (`int`): The token ID representing the pad token. context_length (`int`): The length of the context in the query responses. Returns: tuple: - `reward_logits` (`torch.Tensor`): The logits for the reward model. - `final_rewards` (`torch.Tensor`): The final rewards for each query response. - `sequence_lengths` (`torch.Tensor`): The lengths of the sequences in the query responses. """ attention_mask = query_responses != pad_token_id position_ids = attention_mask.cumsum(1) - attention_mask.long() # exclusive cumsum lm_backbone = getattr(model, model.base_model_prefix) input_ids = torch.masked_fill(query_responses, ~attention_mask, 0) output = lm_backbone( input_ids=input_ids, attention_mask=attention_mask, position_ids=position_ids, return_dict=True, output_hidden_states=True, use_cache=False, # otherwise mistral-based RM would error out ) reward_logits = model.score(output.hidden_states[-1]) sequence_lengths = first_true_indices(query_responses[:, context_length:] == pad_token_id) - 1 + context_length # https://github.com/huggingface/transformers/blob/dc68a39c8111217683bf49a4912d0c9018bab33d/src/transformers/models/gpt2/modeling_gpt2.py#L1454 return ( reward_logits, reward_logits[ torch.arange(reward_logits.size(0), device=reward_logits.device), sequence_lengths, ].squeeze(-1), sequence_lengths, ) def prepare_model_for_kbit_training(model, use_gradient_checkpointing=True, gradient_checkpointing_kwargs=None): r""" Prepare a k-bit quantized transformers model for training (PEFT/QLoRA). """ loaded_in_kbit = getattr(model, "is_loaded_in_8bit", False) or getattr(model, "is_loaded_in_4bit", False) quant_methods = ["gptq", "aqlm", "eetq", "torchao", "hqq"] is_quantized = getattr(model, "quantization_method", None) in quant_methods or getattr( model, "hqq_quantized", False ) if gradient_checkpointing_kwargs is None: gradient_checkpointing_kwargs = {} for _, param in model.named_parameters(): # freeze all parameters param.requires_grad = False # Enable gradient checkpointing if needed if (loaded_in_kbit or is_quantized) and use_gradient_checkpointing: if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: # backward-compatible hook def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) supports_gc_kwargs = "gradient_checkpointing_kwargs" in list( inspect.signature(model.gradient_checkpointing_enable).parameters ) gc_kwargs = {"gradient_checkpointing_kwargs": gradient_checkpointing_kwargs} if supports_gc_kwargs else {} model.gradient_checkpointing_enable(**gc_kwargs) return model def enable_gradient_checkpointing( model: PreTrainedModel, gradient_checkpointing_kwargs: dict | None ) -> PreTrainedModel: """Enables gradient checkpointing for the model.""" # Enable gradient checkpointing on the base model for PEFT if is_peft_model(model): model.base_model.gradient_checkpointing_enable() # Enable gradient checkpointing for non-PEFT models else: model.gradient_checkpointing_enable() gradient_checkpointing_kwargs = gradient_checkpointing_kwargs or {} use_reentrant = ( "use_reentrant" not in gradient_checkpointing_kwargs or gradient_checkpointing_kwargs["use_reentrant"] ) if use_reentrant: if hasattr(model, "enable_input_require_grads"): model.enable_input_require_grads() else: def make_inputs_require_grad(module, input, output): output.requires_grad_(True) model.get_input_embeddings().register_forward_hook(make_inputs_require_grad) return model def prepare_peft_model( model: PreTrainedModel, peft_config: "PeftConfig | None", args: TrainingArguments ) -> PreTrainedModel: """Prepares a model for PEFT training.""" if not is_peft_available(): raise ImportError("PEFT is required to use a peft model. Run `pip install peft`.") if isinstance(model, PeftModel) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge and " "unload the existing adapter, save the resulting base model, and then pass that base model along with the " "new `peft_config` to the trainer." ) # Handle quantized models (QLoRA) is_qlora = getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False) is_sharded_qlora = False if getattr(model, "is_loaded_in_4bit", False): # Check if model is sharded (FSDP/DS-Zero3) for _, param in model.named_parameters(): if param.__class__.__name__ == "Params4bit": is_sharded_qlora = param.data.device.type in {"cpu", "meta"} break # Prepare model for kbit training if needed if is_qlora and not is_sharded_qlora and not isinstance(model, PeftModel): model = prepare_model_for_kbit_training( model, use_gradient_checkpointing=args.gradient_checkpointing, gradient_checkpointing_kwargs=args.gradient_checkpointing_kwargs or {}, ) # Disable gradient checkpointing as it's handled by prepare_model_for_kbit_training args.gradient_checkpointing = False elif args.gradient_checkpointing: model = enable_gradient_checkpointing(model, args.gradient_checkpointing_kwargs) # Create PEFT model if peft_config is not None: if ( Version(peft.__version__) >= Version("0.12") # autocast_adapter_dtype introduced in 0.12 and getattr(model, "is_loaded_in_4bit", False) and is_sharded_qlora ): model = get_peft_model(model, peft_config, autocast_adapter_dtype=False) else: model = get_peft_model(model, peft_config) # Handle bf16 casting for 4-bit models if args.bf16 and getattr(model, "is_loaded_in_4bit", False) and not is_sharded_qlora: peft_module_casting_to_bf16(model) return model def pad_to_length(tensor: torch.Tensor, length: int, pad_value: int | float, dim: int = -1) -> torch.Tensor: if tensor.size(dim) >= length: return tensor else: pad_size = list(tensor.shape) pad_size[dim] = length - tensor.size(dim) return torch.cat( [ tensor, pad_value * torch.ones(*pad_size, dtype=tensor.dtype, device=tensor.device), ], dim=dim, ) def empty_cache() -> None: """Empties the cache of the available torch device. This function checks for the availability of different torch devices (XPU, MLU, NPU, CUDA) and empties the cache of the first available device it finds. If none of the specific devices are available, it defaults to emptying the CUDA cache. """ if is_torch_xpu_available(): torch.xpu.empty_cache() elif is_torch_mlu_available(): torch.mlu.empty_cache() elif is_torch_npu_available(): torch.npu.empty_cache() else: torch.cuda.empty_cache() def peft_module_casting_to_bf16(model): for name, module in model.named_modules(): if isinstance(module, torch.nn.LayerNorm) or "norm" in name: module = module.to(torch.float32) elif any(x in name for x in ["lm_head", "embed_tokens", "wte", "wpe"]): if hasattr(module, "weight"): if module.weight.dtype == torch.float32: module = module.to(torch.bfloat16) LAYER_PATTERNS = [ "transformer.h.{layer}", "model.decoder.layers.{layer}", "gpt_neox.layers.{layer}", "model.layers.{layer}", ] def create_reference_model( model: nn.Module, num_shared_layers: int | None = None, pattern: str | None = None ) -> nn.Module: """ Creates a static reference copy of a model. Note that model will be in `.eval()` mode. Args: model ([`nn.Module`]): The model to be copied. num_shared_layers (`int`, *optional*): The number of initial layers that are shared between both models and kept frozen. pattern (`str`, *optional*): The shared layers are selected with a string pattern (e.g. "transformer.h.{layer}" for GPT2) and if a custom pattern is necessary it can be passed here. Returns: [`nn.Module`] """ if is_deepspeed_zero3_enabled(): raise ValueError( "DeepSpeed ZeRO-3 is enabled and is not compatible with `create_reference_model()`. Please instantiate your reference model directly with `AutoModelForCausalLM.from_pretrained()`." ) parameter_names = [n for n, _ in model.named_parameters()] ref_model = deepcopy(model) # if no layers are shared, return copy of model if num_shared_layers is None: for param_name in parameter_names: param = ref_model.get_parameter(param_name) param.requires_grad = False return ref_model.eval() # identify layer name pattern if pattern is not None: pattern = pattern.format(layer=num_shared_layers) else: for pattern_candidate in LAYER_PATTERNS: pattern_candidate = pattern_candidate.format(layer=num_shared_layers) if any(pattern_candidate in name for name in parameter_names): pattern = pattern_candidate break if pattern is None: raise ValueError("Layer pattern could not be matched.") # divide parameters in shared and unshared parameter lists shared_param_list = [] unshared_param_list = [] shared_parameter = True for name, _param in model.named_parameters(): if pattern in name: shared_parameter = False if shared_parameter: shared_param_list.append(name) else: unshared_param_list.append(name) # create reference of the original parameter if they are shared for param_name in shared_param_list: param = model.get_parameter(param_name) param.requires_grad = False _ref_param = ref_model.get_parameter(param_name) # for all other parameters just make sure they don't use gradients for param_name in unshared_param_list: param = ref_model.get_parameter(param_name) param.requires_grad = False if pattern is not None and len(unshared_param_list) == 0: logging.warning("Pattern passed or found, but no layers matched in the model. Check for a typo.") return ref_model.eval() ================================================ FILE: trl/experimental/winrate_callback.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import pandas as pd from accelerate import Accelerator from accelerate.utils import gather_object, is_wandb_available from transformers import ( GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, Trainer, TrainerCallback, TrainerControl, TrainerState, TrainingArguments, ) from ..models.utils import unwrap_model_for_generation from ..trainer.utils import log_table_to_comet_experiment if is_wandb_available(): import wandb # Logger for module-level logging logger = logging.getLogger(__name__) def _generate_completions( prompts: list[str], model: PreTrainedModel, tokenizer: PreTrainedTokenizerBase, accelerator: Accelerator, generation_config: GenerationConfig | None, batch_size: int = 1, ) -> list[str]: """ Generates completions for a list of pre-formatted prompts from the given model. Args: prompts (list[str]): A list of input prompts for which completions are to be generated. model (PreTrainedModel): The pre-trained model to be used for generation. tokenizer (PreTrainedTokenizerBase): The tokenizer to be used for encoding and decoding. accelerator (Accelerator): The accelerator to be used for model execution. generation_config (GenerationConfig): Configuration for text generation. batch_size (int, optional): The number of prompts to process in each batch. Default is 1. Returns: list[str]: A list of generated text completions corresponding to the input prompts. """ completions = [] # TODO: Override model.generation_config with generation_kwargs with unwrap_model_for_generation(model, accelerator) as unwrapped_model: for idx in range(0, len(prompts), batch_size): batch = prompts[idx : idx + batch_size] tokenized_batch = tokenizer(batch, return_tensors="pt", padding=True, truncation=True).to(model.device) generations = unwrapped_model.generate( **tokenized_batch, generation_config=generation_config, ) for prompt, generation in zip(tokenized_batch.input_ids, generations, strict=True): # Remove prompt from generation generation = generation[len(prompt) :] completion = tokenizer.decode(generation, skip_special_tokens=True) completions.append(completion) return completions def _win_rate_completions_df( state: TrainerState, prompts: list[str], completions: list[str], winner_indices: list[str] ) -> pd.DataFrame: global_step = [str(state.global_step)] * len(prompts) data = list(zip(global_step, prompts, completions, winner_indices, strict=True)) # Split completions from reference model and policy split_data = [(item[0], item[1], item[2][0], item[2][1], item[3]) for item in data] return pd.DataFrame(split_data, columns=["step", "prompt", "reference_model", "policy", "winner_index"]) class WinRateCallback(TrainerCallback): """ A [`~transformers.TrainerCallback`] that computes the win rate of a model based on a reference. It generates completions using prompts from the evaluation dataset and compares the trained model's outputs against a reference. The reference is either the initial version of the model (before training) or the reference model, if available in the trainer. During each evaluation step, a judge determines how often the trained model's completions win against the reference using a judge. The win rate is then logged in the trainer's logs under the key `"eval_win_rate"`. Usage: ```python from trl import DPOTrainer from trl.experimental.judges import PairRMJudge from trl.experimental.winrate_callback import WinRateCallback trainer = DPOTrainer(...) judge = PairRMJudge() win_rate_callback = WinRateCallback(judge=judge, trainer=trainer) trainer.add_callback(win_rate_callback) ``` Args: judge ([`experimental.judges.BasePairwiseJudge`]): The judge to use for comparing completions. trainer (`Trainer`): Trainer to which the callback will be attached. The trainer's evaluation dataset must include a `"prompt"` column containing the prompts for generating completions. If the `Trainer` has a reference model (via the `ref_model` attribute), it will use this reference model for generating the reference completions; otherwise, it defaults to using the initial model. generation_config ([`~transformers.GenerationConfig`], *optional*): The generation config to use for generating completions. num_prompts (`int`, *optional*): The number of prompts to generate completions for. If not provided, defaults to the number of examples in the evaluation dataset. shuffle_order (`bool`, *optional*, defaults to `True`): Whether to shuffle the order of the completions before judging. use_soft_judge (`bool`, *optional*, defaults to `False`): Whether to use a soft judge that returns a win probability between 0 and 1 for the first completion vs the second. """ def __init__( self, judge, trainer: Trainer, generation_config: GenerationConfig | None = None, num_prompts: int | None = None, shuffle_order: bool = True, use_soft_judge: bool = False, ): self.judge = judge self.trainer = trainer self.shuffle_order = shuffle_order self.generation_config = generation_config self.ref_completions = [] self.use_soft_judge = use_soft_judge if self.trainer.eval_dataset is None: raise ValueError("Trainer must have an evaluation dataset to use the WinRateCallback.") else: self.eval_dataset = self.trainer.eval_dataset if num_prompts is not None: self.eval_dataset = self.eval_dataset.select(range(num_prompts)) def on_train_begin(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs): # When the trainer is initialized, we generate completions for the reference model. tokenizer = kwargs["processing_class"] tokenizer.padding_side = "left" accelerator = self.trainer.accelerator # Use the reference model if available, otherwise use the initial model model = getattr(self.trainer, "ref_model", None) # At this point, there are two cases where `ref_model` is None: # 1. The method doesn't require a reference model. # 2. The method uses a reference model, but `ref_model` is set to None. # This occurs when using PEFT, where the reference model can be obtained by simply disabling the model's adapter. # In theory, we should disable the adapter here, but since it's zero-initialized at the start of training, # the model behaves identically with or without the adapter. # Therefore, there's no need to explicitly disable it at this point. if model is None: model = self.trainer.model_wrapped with accelerator.split_between_processes(self.eval_dataset["prompt"]) as prompts: self.ref_completions = _generate_completions( prompts, model=model, tokenizer=tokenizer, accelerator=accelerator, generation_config=self.generation_config, batch_size=args.per_device_eval_batch_size, ) # Compute initial win rate as a reference point completions = list(zip(self.ref_completions, self.ref_completions, strict=True)) if self.use_soft_judge: ref_win_probs = self.judge.judge(prompts, completions, self.shuffle_order, return_scores=True) winner_indices = [0 if score > 0.5 else 1 for score in ref_win_probs] ref_win_probs = gather_object(ref_win_probs) else: winner_indices = self.judge.judge(prompts, completions, self.shuffle_order) prompts = gather_object(prompts) completions = gather_object(completions) winner_indices = gather_object(winner_indices) # Logging if self.trainer.accelerator.is_main_process: win_rate = sum(winner_idx == 1 for winner_idx in winner_indices) / len(winner_indices) if self.use_soft_judge: avg_win_prob = 1.0 - sum(ref_win_probs) / len(ref_win_probs) self.trainer.log({"eval_avg_win_prob": avg_win_prob, "eval_win_rate": win_rate}) else: self.trainer.log({"eval_win_rate": win_rate}) if "wandb" in args.report_to: if wandb.run is not None: df = _win_rate_completions_df( state=state, prompts=prompts, completions=completions, winner_indices=winner_indices, ) wandb.log({"win_rate_completions": wandb.Table(dataframe=df)}) if "comet_ml" in args.report_to: df = _win_rate_completions_df( state=state, prompts=prompts, completions=completions, winner_indices=winner_indices, ) log_table_to_comet_experiment( name="win_rate_completions.csv", table=df, ) def on_evaluate(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs): # At every evaluation step, we generate completions for the model and compare them with the reference # completions that have been generated at the beginning of training. We then compute the win rate and log it to # the trainer. tokenizer = kwargs["processing_class"] tokenizer.padding_side = "left" accelerator = self.trainer.accelerator model = self.trainer.model_wrapped with accelerator.split_between_processes(self.eval_dataset["prompt"]) as prompts: completions = _generate_completions( prompts, model=model, tokenizer=tokenizer, accelerator=accelerator, generation_config=self.generation_config, batch_size=args.per_device_eval_batch_size, ) completions = list(zip(self.ref_completions, completions, strict=True)) if self.use_soft_judge: ref_win_probs = self.judge.judge(prompts, completions, self.shuffle_order, return_scores=True) winner_indices = [0 if score > 0.5 else 1 for score in ref_win_probs] ref_win_probs = gather_object(ref_win_probs) else: winner_indices = self.judge.judge(prompts, completions, self.shuffle_order) prompts = gather_object(prompts) completions = gather_object(completions) winner_indices = gather_object(winner_indices) # Logging if self.trainer.accelerator.is_main_process: win_rate = sum(winner_idx == 1 for winner_idx in winner_indices) / len(winner_indices) if self.use_soft_judge: avg_win_prob = 1.0 - sum(ref_win_probs) / len(ref_win_probs) self.trainer.log({"eval_avg_win_prob": avg_win_prob, "eval_win_rate": win_rate}) else: self.trainer.log({"eval_win_rate": win_rate}) if "wandb" in args.report_to: if wandb.run is not None: df = _win_rate_completions_df( state=state, prompts=prompts, completions=completions, winner_indices=winner_indices, ) wandb.log({"win_rate_completions": wandb.Table(dataframe=df)}) if "comet_ml" in args.report_to: df = _win_rate_completions_df( state=state, prompts=prompts, completions=completions, winner_indices=winner_indices, ) log_table_to_comet_experiment( name="win_rate_completions.csv", table=df, ) ================================================ FILE: trl/experimental/xpo/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .xpo_config import XPOConfig from .xpo_trainer import XPOTrainer __all__ = ["XPOConfig", "XPOTrainer"] ================================================ FILE: trl/experimental/xpo/xpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from ..online_dpo import OnlineDPOConfig @dataclass class XPOConfig(OnlineDPOConfig): r""" Configuration class for the [`experimental.xpo.XPOTrainer`]. Subclass of [`experimental.online_dpo.OnlineDPOConfig`] we can use all its arguments and add the following: Parameters: alpha (`float` or `list[float]`, *optional*, defaults to `1e-5`): Weight of the XPO loss term. If a list of floats is provided then the alpha is selected for each new epoch and the last alpha is used for the rest of the epochs. """ alpha: list[float] = field( default_factory=lambda: [1e-5], metadata={ "help": "Weight of the XPO loss term. If a list of floats is provided then the alpha is selected for each " "new epoch and the last alpha is used for the rest of the epochs." }, ) def __post_init__(self): super().__post_init__() if hasattr(self.alpha, "__len__") and len(self.alpha) == 1: self.alpha = self.alpha[0] ================================================ FILE: trl/experimental/xpo/xpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import textwrap from collections.abc import Callable from typing import Any import jinja2 import torch import torch.nn as nn import torch.nn.functional as F from datasets import Dataset, IterableDataset from transformers import ( BaseImageProcessor, FeatureExtractionMixin, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, ) from transformers.trainer_utils import EvalPrediction from transformers.training_args import OptimizerNames from transformers.utils import is_peft_available from ...data_utils import is_conversational, maybe_apply_chat_template from ...models.utils import unwrap_model_for_generation from ...trainer.utils import selective_log_softmax from ..judges import BasePairwiseJudge from ..online_dpo import OnlineDPOTrainer from ..utils import SIMPLE_CHAT_TEMPLATE, empty_cache, get_reward, truncate_right from .xpo_config import XPOConfig if is_peft_available(): from peft import PeftModel class XPOTrainer(OnlineDPOTrainer): """ Trainer for Exploratory Preference Optimization (XPO). It is implemented as a subclass of [`experimental.online_dpo.OnlineDPOTrainer`]. Args: model ([`~transformers.PreTrainedModel`]): The model to train, preferably an `AutoModelForCausalLM`. ref_model ([`~transformers.PreTrainedModel`]): Hugging Face transformer model with a casual language modelling head. Used for implicit reward computation and loss. If no reference model is provided, the trainer will create a reference model with the same architecture as the model to be optimized. reward_funcs ([`~transformers.PreTrainedModel`]): The reward model to score completions with, preferably an [`~transformers.AutoModelForSequenceClassification`]. judge ([`experimental.judges.BasePairwiseJudge`]): The judge to use for pairwise comparison of model completions. args ([`experimental.xpo.XPOConfig`]): The XPO config arguments to use for training. data_collator ([`~transformers.DataCollator`]): The data collator to use for training. If None is specified, the default data collator ([`experimental.utils.DPODataCollatorWithPadding`]) will be used which will pad the sequences to the maximum length of the sequences in the batch, given a dataset of paired sequences. train_dataset ([`~datasets.Dataset`]): The dataset to use for training. eval_dataset ([`~datasets.Dataset`]): The dataset to use for evaluation. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.BaseImageProcessor`], [`~transformers.FeatureExtractionMixin`] or [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If provided, will be used to automatically process the inputs for the model, and it will be saved along the model to make it easier to rerun an interrupted training or reuse the fine-tuned model. peft_config (`dict`): The peft config to use for training. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function to use to compute the metrics. Must take a `EvalPrediction` and return a dictionary string to metric values. callbacks (`list[transformers.TrainerCallback]`): The callbacks to use for training. optimizers (`tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR]`): The optimizer and scheduler to use for training. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`): The function to use to preprocess the logits before computing the metrics. """ _tag_names = ["trl", "xpo"] _name = "XPO" _paper = { "title": "Exploratory Preference Optimization: Harnessing Implicit Q*-Approximation for Sample-Efficient RLHF", "id": "2405.21046", # docstyle-ignore "citation": textwrap.dedent("""\ @article{jung2024binary, title = {{Exploratory Preference Optimization: Harnessing Implicit Q*-Approximation for Sample-Efficient RLHF}}, author = {Tengyang Xie and Dylan J. Foster and Akshay Krishnamurthy and Corby Rosset and Ahmed Awadallah and Alexander Rakhlin}, year = 2024, eprint = {arXiv:2405.21046} }"""), } def __init__( self, model: PreTrainedModel | nn.Module = None, ref_model: PreTrainedModel | nn.Module = None, reward_funcs: nn.Module | None = None, judge: BasePairwiseJudge | None = None, args: XPOConfig | None = None, data_collator: Callable | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | dict[str, Dataset] | None = None, processing_class: PreTrainedTokenizerBase | BaseImageProcessor | FeatureExtractionMixin | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, peft_config: dict | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None), preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, ) -> None: super().__init__( model=model, ref_model=ref_model, judge=judge, reward_funcs=reward_funcs, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, reward_processing_classes=reward_processing_classes, peft_config=peft_config, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) self._alpha = self.args.alpha # Overwrite the stats dictionary to include XPO specific statistics self.stats = { # Remove "non_score_reward", "rlhf_reward", "scores" # Add "loss/dpo", "loss/xpo" "loss/dpo": [], "loss/xpo": [], "objective/kl": [], "objective/entropy": [], "rewards/chosen": [], "rewards/rejected": [], "rewards/accuracies": [], "rewards/margins": [], "logps/chosen": [], "logps/rejected": [], # Replace "contain_eos_token" by "model_contain_eos_token" and "ref_contain_eos_token" "val/model_contain_eos_token": [], "val/ref_contain_eos_token": [], "alpha": [], "beta": [], } if self.reward_funcs is not None: if len(self.reward_funcs) != 1: raise ValueError("XPOTrainer only supports one reward function/model.") self.reward_funcs = self.reward_funcs[0] self.stats["objective/model_scores"] = [] self.stats["objective/ref_scores"] = [] self.stats["objective/scores_margin"] = [] @property def alpha(self): if isinstance(self._alpha, list): epoch = self.state.epoch return self._alpha[epoch] if epoch < len(self._alpha) else self._alpha[-1] else: return self._alpha def _generate_completions(self, prompts, model): with ( unwrap_model_for_generation( model, self.accelerator, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_policy_model_for_gen, ): model_output = unwrapped_policy_model_for_gen.generate( input_ids=prompts["input_ids"], attention_mask=prompts["attention_mask"], generation_config=self.generation_config, ) actual_model_for_ref_generation: torch.nn.Module if self.ref_model is None: unwrapped_main_model_for_ref_logic = self.accelerator.unwrap_model(model) if is_peft_available() and isinstance(unwrapped_main_model_for_ref_logic, PeftModel): actual_model_for_ref_generation = unwrapped_main_model_for_ref_logic.get_base_model() else: actual_model_for_ref_generation = unwrapped_main_model_for_ref_logic else: actual_model_for_ref_generation = self.accelerator.unwrap_model(self.ref_model) with ( unwrap_model_for_generation( actual_model_for_ref_generation, self.accelerator, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as final_ref_model_for_gen, ): ref_output = final_ref_model_for_gen.generate( input_ids=prompts["input_ids"], attention_mask=prompts["attention_mask"], generation_config=self.generation_config, ) return model_output, ref_output def _process_completions(self, model_output, ref_output, prompts): context_length = prompts["input_ids"].shape[1] # Process model completions model_completion_ids = model_output[:, context_length:] model_completion_ids, model_completion_mask = truncate_right( model_completion_ids, self.processing_class.eos_token_id, self.processing_class.pad_token_id ) model_data = { "input_ids": torch.cat((prompts["input_ids"], model_completion_ids), dim=1), "attention_mask": torch.cat((prompts["attention_mask"], model_completion_mask), dim=1), "raw": prompts["raw"], } # Process reference model completions ref_completion_ids = ref_output[:, context_length:] ref_completion_ids, ref_completion_mask = truncate_right( ref_completion_ids, self.processing_class.eos_token_id, self.processing_class.pad_token_id ) ref_data = { "input_ids": torch.cat((prompts["input_ids"], ref_completion_ids), dim=1), "attention_mask": torch.cat((prompts["attention_mask"], ref_completion_mask), dim=1), "raw": prompts["raw"], } return model_data, ref_data def _compute_rewards(self, model_data, ref_data, context_length): with torch.no_grad(): _, model_scores, _ = get_reward( self.reward_funcs, model_data["input_ids"], self.processing_class.pad_token_id, context_length ) _, ref_scores, _ = get_reward( self.reward_funcs, ref_data["input_ids"], self.processing_class.pad_token_id, context_length ) # Apply EOS penalty if needed if self.args.missing_eos_penalty is not None: model_contain_eos = torch.any(model_data["input_ids"] == self.processing_class.eos_token_id, dim=-1) ref_contain_eos = torch.any(ref_data["input_ids"] == self.processing_class.eos_token_id, dim=-1) model_scores[~model_contain_eos] -= self.args.missing_eos_penalty ref_scores[~ref_contain_eos] -= self.args.missing_eos_penalty return model_scores, ref_scores def _compute_judge(self, model_data, ref_data, context_length): prompts = model_data["raw"] model_data_completions = self.processing_class.batch_decode( model_data["input_ids"][:, context_length:], skip_special_tokens=True ) model_data_completions = [completion.strip() for completion in model_data_completions] ref_data_completions = self.processing_class.batch_decode( ref_data["input_ids"][:, context_length:], skip_special_tokens=True ) ref_data_completions = [completion.strip() for completion in ref_data_completions] if is_conversational({"prompt": prompts[0]}): model_data_completions = [ [{"role": "assistant", "content": completion}] for completion in model_data_completions ] environment = jinja2.Environment() template = environment.from_string(SIMPLE_CHAT_TEMPLATE) prompts = [template.render(messages=message) for message in prompts] model_data_completions = [template.render(messages=completion) for completion in model_data_completions] ref_data_completions = [ [{"role": "assistant", "content": completion}] for completion in ref_data_completions ] ref_data_completions = [template.render(messages=completion) for completion in ref_data_completions] ranks_of_first_completion = self.judge.judge( prompts, list(zip(model_data_completions, ref_data_completions, strict=True)), ) # convert ranks to a True/False mask: # when rank == 0, it means the first completion is the best # when rank == 1, it means the second completion is the best return torch.tensor([rank == 0 for rank in ranks_of_first_completion], device=model_data["input_ids"].device) def _compute_logprobs(self, model, model_data, ref_data, context_length): def compute_logprobs_for_data(m, data): output = m(data["input_ids"], attention_mask=data["attention_mask"]) logits = output.logits[:, context_length - 1 : -1] token_logprobs = selective_log_softmax(logits, data["input_ids"][:, context_length:]) return token_logprobs # Compute logprobs for model completions model_logprobs_model_data = compute_logprobs_for_data(model, model_data) # Compute logprobs for model on reference completions (for XPO loss) model_logprobs_ref_data = compute_logprobs_for_data(model, ref_data) # Compute logprobs for reference model completions with torch.no_grad(): if self.ref_model is None: with model.disable_adapter(): ref_logprobs_model_data = compute_logprobs_for_data(model, model_data) ref_logprobs_ref_data = compute_logprobs_for_data(model, ref_data) else: ref_logprobs_model_data = compute_logprobs_for_data(self.ref_model, model_data) ref_logprobs_ref_data = compute_logprobs_for_data(self.ref_model, ref_data) # Mask padding tokens model_padding_mask = model_data["attention_mask"][:, context_length:] == 0 ref_padding_mask = ref_data["attention_mask"][:, context_length:] == 0 model_logprobs_model_data = model_logprobs_model_data.masked_fill(model_padding_mask, 0.0) model_logprobs_ref_data = model_logprobs_ref_data.masked_fill(ref_padding_mask, 0.0) ref_logprobs_ref_data = ref_logprobs_ref_data.masked_fill(ref_padding_mask, 0.0) ref_logprobs_model_data = ref_logprobs_model_data.masked_fill(model_padding_mask, 0.0) return model_logprobs_model_data, model_logprobs_ref_data, ref_logprobs_ref_data, ref_logprobs_model_data def _compute_losses( self, model_logprobs_model_data, model_logprobs_ref_data, ref_logprobs_ref_data, ref_logprobs_model_data, chosen_mask, ): # Compute log probs model_logprobs_model_data_sum = model_logprobs_model_data.sum(1) model_logprobs_ref_data_sum = model_logprobs_ref_data.sum(1) ref_logprobs_ref_data_sum = ref_logprobs_ref_data.sum(1) ref_logprobs_model_data_sum = ref_logprobs_model_data.sum(1) chosen_model_logprobs = torch.where(chosen_mask, model_logprobs_model_data_sum, model_logprobs_ref_data_sum) chosen_ref_logprobs = torch.where(chosen_mask, ref_logprobs_model_data_sum, ref_logprobs_ref_data_sum) chosen_log_ratios = chosen_model_logprobs - chosen_ref_logprobs rejected_model_logprobs = torch.where(~chosen_mask, model_logprobs_model_data_sum, model_logprobs_ref_data_sum) rejected_ref_logprobs = torch.where(~chosen_mask, ref_logprobs_model_data_sum, ref_logprobs_ref_data_sum) rejected_log_ratios = rejected_model_logprobs - rejected_ref_logprobs # Compute logits as the difference between chosen and rejected log ratios logits = chosen_log_ratios - rejected_log_ratios if self.args.loss_type == "sigmoid": dpo_losses = -F.logsigmoid(self.beta * logits) elif self.args.loss_type == "ipo": dpo_losses = (logits - 1 / (2 * self.beta)) ** 2 else: raise NotImplementedError(f"invalid loss type {self.args.loss_type}") # Compute XPO specific loss xpo_losses = self.alpha * model_logprobs_ref_data_sum # Total loss loss = (dpo_losses + xpo_losses).mean() return loss, dpo_losses, xpo_losses def _log_statistics( self, model_data, ref_data, model_logprobs_model_data, model_logprobs_ref_data, ref_logprobs_ref_data, ref_logprobs_model_data, chosen_mask, dpo_losses, xpo_losses, context_length, model_scores=None, ref_scores=None, ): # Helper function to gather and compute mean def gather_mean(tensor): return self.accelerator.gather_for_metrics(tensor).mean().item() # Log losses self.stats["loss/dpo"].append(gather_mean(dpo_losses)) self.stats["loss/xpo"].append(gather_mean(xpo_losses)) # Log scores if self.reward_funcs is not None: self.stats["objective/model_scores"].append(gather_mean(model_scores)) self.stats["objective/ref_scores"].append(gather_mean(ref_scores)) self.stats["objective/scores_margin"].append(gather_mean(model_scores - ref_scores)) # Log logprobs model_logprobs_model_data_sum = model_logprobs_model_data.sum(1) model_logprobs_ref_data_sum = model_logprobs_ref_data.sum(1) ref_logprobs_ref_data_sum = ref_logprobs_ref_data.sum(1) ref_logprobs_model_data_sum = ref_logprobs_model_data.sum(1) chosen_model_logprobs = torch.where(chosen_mask, model_logprobs_model_data_sum, model_logprobs_ref_data_sum) chosen_ref_logprobs = torch.where(chosen_mask, ref_logprobs_model_data_sum, ref_logprobs_ref_data_sum) chosen_log_ratios = chosen_model_logprobs - chosen_ref_logprobs rejected_model_logprobs = torch.where(~chosen_mask, model_logprobs_model_data_sum, model_logprobs_ref_data_sum) rejected_ref_logprobs = torch.where(~chosen_mask, ref_logprobs_model_data_sum, ref_logprobs_ref_data_sum) rejected_log_ratios = rejected_model_logprobs - rejected_ref_logprobs self.stats["logps/chosen"].append(gather_mean(chosen_model_logprobs.mean() + chosen_ref_logprobs.mean())) self.stats["logps/rejected"].append(gather_mean(rejected_model_logprobs.mean() + rejected_ref_logprobs.mean())) # Log rewards # Compute various statistics chosen_rewards = chosen_log_ratios * self.beta rejected_rewards = rejected_log_ratios * self.beta self.stats["rewards/chosen"].append(gather_mean(chosen_rewards.mean())) self.stats["rewards/rejected"].append(gather_mean(rejected_rewards.mean())) # Calculate KL divergence for model and ref data kl_model_data = model_logprobs_model_data - ref_logprobs_model_data kl_ref_data = model_logprobs_ref_data - ref_logprobs_ref_data mean_kl = (kl_model_data.sum(1) + kl_ref_data.sum(1)).mean() / 2 self.stats["objective/kl"].append(gather_mean(mean_kl)) # Calculate entropy for model and ref data entropy_model_data = -model_logprobs_model_data.sum(1) entropy_ref_data = -model_logprobs_ref_data.sum(1) mean_entropy = (entropy_model_data.mean() + entropy_ref_data.mean()) / 2 self.stats["objective/entropy"].append(gather_mean(mean_entropy)) # Calculate margins margin = chosen_rewards - rejected_rewards self.stats["rewards/margins"].append(gather_mean(margin.mean())) # Calculate accuracy accuracy = (margin > 0).float() self.stats["rewards/accuracies"].append(gather_mean(accuracy.mean())) # Log EOS token statistics model_eos = (model_data["input_ids"][:, context_length:] == self.processing_class.eos_token_id).any(dim=1) ref_eos = (ref_data["input_ids"][:, context_length:] == self.processing_class.eos_token_id).any(dim=1) self.stats["val/model_contain_eos_token"].append(gather_mean(model_eos.float())) self.stats["val/ref_contain_eos_token"].append(gather_mean(ref_eos.float())) # Log alpha and beta self.stats["alpha"].append(self.alpha) self.stats["beta"].append(self.beta) def training_step( self, model: nn.Module, inputs: dict[str, torch.Tensor | Any], num_items_in_batch: int | None = None ) -> torch.Tensor: model.train() # Apply chat template and tokenize the input batch_size = len(next(iter(inputs.values()))) prompts = inputs["prompt"] inputs = [{k: v[i] for k, v in inputs.items()} for i in range(batch_size)] inputs = [maybe_apply_chat_template(x, self.processing_class) for x in inputs] inputs = [self.tokenize_row(x, self.model.config.is_encoder_decoder, self.processing_class) for x in inputs] inputs = self.data_collator(inputs) # need the prompt_ only inputs = self._prepare_inputs(inputs) context_length = inputs["prompt_input_ids"].shape[1] prompts = { "input_ids": inputs["prompt_input_ids"], "attention_mask": inputs["prompt_attention_mask"], "raw": prompts, } del inputs # Sample completions from both the model and the reference model model_output, ref_output = self._generate_completions(prompts, model) # Process model completions model_data, ref_data = self._process_completions(model_output, ref_output, prompts) # Compute rewards if self.reward_funcs is not None: model_scores, ref_scores = self._compute_rewards(model_data, ref_data, context_length) chosen_mask = model_scores >= ref_scores else: model_scores, ref_scores = None, None chosen_mask = self._compute_judge(model_data, ref_data, context_length) # Compute logprobs model_logprobs_model_data, model_logprobs_ref_data, ref_logprobs_ref_data, ref_logprobs_model_data = ( self._compute_logprobs(model, model_data, ref_data, context_length) ) # Compute loss loss, dpo_losses, xpo_losses = self._compute_losses( model_logprobs_model_data, model_logprobs_ref_data, ref_logprobs_ref_data, ref_logprobs_model_data, chosen_mask, ) # Log everything self._log_statistics( model_data, ref_data, model_logprobs_model_data.detach(), model_logprobs_ref_data.detach(), ref_logprobs_ref_data, ref_logprobs_model_data, chosen_mask, dpo_losses.detach(), xpo_losses.detach(), context_length, model_scores, ref_scores, ) if ( self.args.torch_empty_cache_steps is not None and self.state.global_step % self.args.torch_empty_cache_steps == 0 ): empty_cache() kwargs = {} # For LOMO optimizers you need to explicitly use the learning rate if self.args.optim in [OptimizerNames.LOMO, OptimizerNames.ADALOMO]: kwargs["learning_rate"] = self._get_learning_rate() if self.args.n_gpu > 1: loss = loss.mean() # mean() to average on multi-gpu parallel training self.accelerator.backward(loss, **kwargs) return loss.detach() / self.args.gradient_accumulation_steps ================================================ FILE: trl/extras/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ================================================ FILE: trl/extras/dataset_formatting.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datasets from datasets import Value from packaging.version import Version if Version(datasets.__version__) >= Version("4.0.0"): from datasets import List FORMAT_MAPPING = { "chatml": List({"content": Value(dtype="string", id=None), "role": Value(dtype="string", id=None)}), "instruction": {"completion": Value(dtype="string", id=None), "prompt": Value(dtype="string", id=None)}, } else: FORMAT_MAPPING = { "chatml": [{"content": Value(dtype="string", id=None), "role": Value(dtype="string", id=None)}], "instruction": {"completion": Value(dtype="string", id=None), "prompt": Value(dtype="string", id=None)}, } ================================================ FILE: trl/extras/profiling.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import time from collections.abc import Callable from transformers import Trainer from transformers.integrations import is_mlflow_available, is_wandb_available if is_wandb_available(): import wandb if is_mlflow_available(): import mlflow class ProfilingContext: """ Context manager for profiling code blocks with configurable logging. This class handles timing of code execution and logging metrics to various backends (Weights & Biases, MLflow) without being coupled to the Trainer class. Args: name (`str`): Name of the profiling context. Used in the metric name. report_to (`list` of `str`): List of integrations to report metrics to (e.g., ["wandb", "mlflow"]). is_main_process (`bool`, *optional*, defaults to `True`): Whether this is the main process in distributed training. Metrics are only logged from the main process. step (`int` or `None`, *optional*): Training step to associate with the logged metrics. metric_prefix (`str`, *optional*, defaults to `"profiling/Time taken"`): Prefix for the metric name in logs. Example: ```python # Direct usage from trl.extras.profiling import ProfilingContext with ProfilingContext( name="MyClass.expensive_operation", report_to=["wandb"], is_main_process=True, step=100, ): # Code to profile result = expensive_computation() # With Trainer (backwards compatible via profiling_context function) from transformers import Trainer from trl.extras.profiling import profiling_context class MyTrainer(Trainer): def some_method(self): with profiling_context(self, "matrix_multiplication"): result = matrix_multiply() ``` """ def __init__( self, name: str, report_to: list[str], is_main_process: bool = True, step: int | None = None, metric_prefix: str = "profiling/Time taken", ): self.name = name self.report_to = report_to self.is_main_process = is_main_process self.step = step self.metric_prefix = metric_prefix self._start_time = None def __enter__(self): """Start timing when entering the context.""" self._start_time = time.perf_counter() return self def __exit__(self, exc_type, exc_val, exc_tb): """Stop timing and log metrics when exiting the context.""" if self._start_time is not None: duration = time.perf_counter() - self._start_time self._log_metrics(duration) return False def _log_metrics(self, duration: float) -> None: """ Log profiling metrics to configured backends. Args: duration (`float`): Execution time in seconds. """ if not self.is_main_process: return metric_name = f"{self.metric_prefix}: {self.name}" metrics = {metric_name: duration} # Log to Weights & Biases if configured if "wandb" in self.report_to and is_wandb_available() and wandb.run is not None: wandb.log(metrics) # Log to MLflow if configured if "mlflow" in self.report_to and is_mlflow_available() and mlflow.active_run() is not None: mlflow.log_metrics(metrics, step=self.step) def profiling_context(trainer: Trainer, name: str) -> ProfilingContext: """ Factory function to create a ProfilingContext from a Trainer instance. This function maintains backwards compatibility with existing code while using the decoupled ProfilingContext class internally. Args: trainer (`~transformers.Trainer`): Trainer object containing configuration for logging. name (`str`): Name of the block to be profiled. Will be prefixed with the trainer class name. Returns: `ProfilingContext`: A configured profiling context manager. Example: ```python from transformers import Trainer from trl.extras.profiling import profiling_context class MyTrainer(Trainer): def some_method(self): A = np.random.rand(1000, 1000) B = np.random.rand(1000, 1000) with profiling_context(self, "matrix_multiplication"): # Code to profile: simulate a computationally expensive operation result = A @ B # Matrix multiplication ``` """ context_name = f"{trainer.__class__.__name__}.{name}" step = trainer.state.global_step return ProfilingContext( name=context_name, report_to=trainer.args.report_to, is_main_process=trainer.accelerator.is_main_process, step=step, ) def profiling_decorator(func: Callable) -> Callable: """ Decorator to profile a function and log execution time using [`extras.profiling.profiling_context`]. This decorator works with methods that have access to a trainer instance (typically as `self`). For non-Trainer objects that have an `accelerator` attribute, it will use that for logging configuration. Args: func (`Callable`): Function to be profiled. Returns: `Callable`: Wrapped function that profiles execution time. Example: ```python from transformers import Trainer from trl.extras.profiling import profiling_decorator class MyTrainer(Trainer): @profiling_decorator def some_method(self): A = np.random.rand(1000, 1000) B = np.random.rand(1000, 1000) # Code to profile: simulate a computationally expensive operation result = A @ B ``` """ @functools.wraps(func) def wrapper(self, *args, **kwargs): # Check if self is a Trainer-like object with required attributes if hasattr(self, "state") and hasattr(self, "args"): with profiling_context(self, func.__name__): return func(self, *args, **kwargs) # For non-Trainer objects (e.g., VLLMGeneration), use ProfilingContext directly elif hasattr(self, "accelerator"): context_name = f"{self.__class__.__name__}.{func.__name__}" with ProfilingContext( name=context_name, report_to=[], # No reporting for non-Trainer objects without args is_main_process=self.accelerator.is_main_process, step=None, ): return func(self, *args, **kwargs) else: # No profiling available, just run the function return func(self, *args, **kwargs) return wrapper ================================================ FILE: trl/generation/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Generation backends for TRL trainers.""" from ..import_utils import is_vllm_available __all__ = [] if is_vllm_available(): from .vllm_generation import VLLMGeneration __all__.append("VLLMGeneration") ================================================ FILE: trl/generation/vllm_client.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import atexit import base64 import copy import logging import socket import time from io import BytesIO from urllib.parse import urlparse import torch import torch.distributed.distributed_c10d as c10d from requests.adapters import HTTPAdapter from torch import nn from transformers import is_torch_xpu_available from transformers.utils import get_json_schema from urllib3.util.retry import Retry from ..import_utils import is_requests_available, is_vllm_ascend_available, is_vllm_available if is_requests_available(): import requests from requests import ConnectionError if is_vllm_available(): from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator from vllm.distributed.utils import StatelessProcessGroup if is_vllm_ascend_available(): from vllm_ascend.distributed.device_communicators.pyhccl import PyHcclCommunicator as PyNcclCommunicator logger = logging.getLogger(__name__) def pil_to_base64(image): buffer = BytesIO() image.save(buffer, format="PNG") img_bytes = buffer.getvalue() return base64.b64encode(img_bytes).decode("utf-8") class VLLMClient: """ A client class to interact with a vLLM server. This class provides methods to generate completions, initialize and manage weight update groups, and update model weights in a distributed setting. Before using it, start the vLLM server with `trl vllm-serve`. Args: base_url (`str`, *optional*): Base URL for the vLLM server (e.g., `"http://localhost:8000"`). If provided, `host` and `server_port` are ignored. host (`str`, *optional*, defaults to `"0.0.0.0"`): IP address of the vLLM server. Ignored if `base_url` is provided. server_port (`int`, *optional*, defaults to `8000`): Port number of the vLLM server. Ignored if `base_url` is provided. group_port (`int`, *optional*, defaults to `51216`): Port number for the weight update group. connection_timeout (`float`, *optional*, defaults to `0.0`): Total timeout duration in seconds to wait for the server to be up. If the server is not up after the timeout, a `ConnectionError` is raised. Examples: Run the vLLM server with the model `Qwen/Qwen2.5-7B`: ``` $ trl vllm-serve --model Qwen/Qwen2.5-7B ... INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) ``` Use the client to generate completions and update model weights: ```python >>> from trl.generation.vllm_client import VLLMClient >>> client = VLLMClient() >>> client.generate(["Hello, AI!", "Tell me a joke"]) {'prompt_ids': [[9707, 11, 15235, 0], [40451, 752, 264, 21646]], 'completion_ids': [[2980, 498, 1492, 752, 448, 264, 13027, 8645, 30, 358, 2776, 4460, 311, 3270, 264, 2025], [911, 98072, 2142, 624, 45, 51426, 2142, 374, 279, 16396, 429, 4302, 702, 36988, 7290, 476]], 'logprobs': [[[-1.6612], [-0.0081], [-1.5189], [-0.0123], [-1.2045], [-0.6227], [-2.9791], [-2.8387], [-0.1267], [-0.0366], [-2.6528], [-0.3197], [-0.0001], [-1.8174], [-0.0251], [-1.473]], [[-0.018], [-10.7331], [-0.1605], [-0.891], [-3.7945], [-0.0127], [-0.3073], [-1.1648], [-1.8025], [-0.409], [-0.0256], [-1.6127], [-2.2935], [-4.1785], [-0.6531], [-0.2629]]], 'logprob_token_ids': [[[2980], [498], [1492], [752], [448], [264], [13027], [8645], [30], [358], [2776], [4460], [311], [3270], [264], [2025]], [[911], [98072], [2142], [624], [45], [51426], [2142], [374], [279], [16396], [429], [4302], [702], [36988], [7290], [476]]]} >>> from transformers import AutoModelForCausalLM >>> model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B", device_map="cuda") >>> client.init_communicator(device="cuda") >>> client.update_model_params(model) ``` There are several ways to initialize the client: ```python VLLMClient(base_url="http://localhost:8000") VLLMClient(base_url="http://192.168.1.100:8000") VLLMClient(host="localhost", server_port=8000) VLLMClient(host="192.168.1.100", server_port=8000) ``` """ def __init__( self, base_url: str | None = None, host: str = "0.0.0.0", server_port: int = 8000, group_port: int = 51216, connection_timeout: float = 0.0, ): if not is_requests_available(): raise ImportError("requests is not installed. Please install it with `pip install requests`.") if not is_vllm_available(): raise ImportError("vLLM is not installed. Please install it with `pip install trl[vllm]`.") self.session = requests.Session() # Configure retries for HTTP requests made through this session. # This is not strictly required for correctness, but it helps make training more robust to rare, transient # failures (network hiccups, temporary 5xx errors, overloaded servers). Without this, such failures could cause # an otherwise healthy training run to fail. retry_strategy = Retry( total=5, # global cap on the total number of retries across all failure types connect=5, # retry connection-level failures (DNS issues, refused connections, etc) read=5, # retry failures while reading the response after the connection was successfully established status=3, # retry a limited number of times when we receive certain HTTP error responses from the server status_forcelist=[500, 502, 503], # only retry on server-side errors that are usually temporary backoff_factor=2, # exponential backoff between retries (2s, 4s, 8s, ...) allowed_methods=["POST", "GET"], # allow POST as well, even though we're not sure it's safe here ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount("http://", adapter) self.session.mount("https://", adapter) if base_url is not None: # Parse the base_url to extract host and port parsed_url = urlparse(base_url) self.host = socket.gethostbyname(parsed_url.hostname) scheme = parsed_url.scheme or "http" self.base_url = f"{scheme}://{parsed_url.netloc}{parsed_url.path}" else: self.host = host self.server_port = server_port self.base_url = f"http://{self.host}:{self.server_port}" self.group_port = group_port self.check_server(connection_timeout) # check server and fail after timeout def check_server(self, total_timeout: float = 0.0, retry_interval: float = 2.0): """ Check server availability with retries on failure, within a total timeout duration. If the server is not up after the total timeout duration, raise a `ConnectionError`. Args: retry_interval (`float`, *optional*, defaults to `2.0`): Interval in seconds between retries. total_timeout (`float`, *optional*, defaults to `0.0`): Total timeout duration in seconds. """ url = f"{self.base_url}/health/" start_time = time.time() # Record the start time while True: try: response = requests.get(url) except requests.exceptions.RequestException as exc: # Check if the total timeout duration has passed elapsed_time = time.time() - start_time if elapsed_time >= total_timeout: raise ConnectionError( f"The vLLM server can't be reached at {self.base_url} after {total_timeout} seconds. Make " "sure the server is running by running `trl vllm-serve`." ) from exc else: if response.status_code == 200: if "X-Forwarded-For" in response.headers: self.host = response.headers["X-Forwarded-For"] logger.info("Server is up!") return None # Retry logic: wait before trying again logger.info(f"Server is not up yet. Retrying in {retry_interval} seconds...") time.sleep(retry_interval) def generate( self, prompts: list[str] | list[list[int]], images: list | None = None, n: int = 1, repetition_penalty: float = 1.0, temperature: float = 1.0, top_p: float = 1.0, top_k: int = 0, min_p: float = 0.0, max_tokens: int = 16, logprobs: int | None = 0, structured_outputs_regex: str | None = None, generation_kwargs: dict | None = None, ) -> dict[str, list[list[int]]]: """ Generates model completions for the provided prompts. Args: prompts (`list[str]` or `list[list[int]]`): List of text prompts or list of token ID lists for which the model will generate completions. images (`list[list[PIL.Image] | None]`, *optional*): List of image lists for VLM support. Each element is a list of PIL images for the corresponding prompt, or `None` if no images for that prompt. n (`int`, *optional*, defaults to `1`): Number of completions to generate for each prompt. repetition_penalty (`float`, *optional*, defaults to `1.0`): Parameter for repetition penalty. 1.0 means no penalty. temperature (`float`, *optional*, defaults to `1.0`): Temperature parameter for sampling. Higher values increase diversity. top_p (`float`, *optional*, defaults to `1.0`): Top-p sampling parameter.`1.0` means no truncation. top_k (`int`, *optional*, defaults to `0`): Top-k sampling parameter. `0` means no truncation. min_p (`float`, *optional*, defaults to `0.0`): Minimum probability for sampling. max_tokens (`int`, *optional*, defaults to `16`): Maximum number of tokens to generate for each prompt. logprobs (`int` or `None`, *optional*, defaults to `0`): Number of top logprobs to return per token. When 0, only the sampled token's logprob is returned. When N>0, returns up to N+1 logprobs sorted by descending probability, because vLLM always includes the sampled token's logprob (which may fall outside the top-N). structured_outputs_regex (`str`, *optional*): Regular expression to guide the decoding process. generation_kwargs (`dict`, *optional*): Additional generation parameters to pass to the vLLM `SamplingParams`. This can include parameters like `seed`, `frequency_penalty`, etc. If it contains keys that conflict with the other parameters, they will override them. Returns: `dict` with keys: - `prompt_ids` (`list[list[int]]`): List of lists of token IDs representing the tokenized input prompts. - `completion_ids` (`list[list[int]]`): List of lists of token IDs representing the model-generated completions for each prompt. - `logprobs` (`list[list[list[float]]]`): Per-token logprobs of shape (num_sequences, seq_len, num_logprobs), sorted by descending probability. - `logprob_token_ids` (`list[list[list[int]]]`): Token IDs corresponding to each logprob, same shape as `logprobs`. """ url = f"{self.base_url}/generate/" # Convert PIL images to base64 strings. Each element is a list of images for the corresponding prompt, # or None if no images for that prompt. if images: images = [ [pil_to_base64(img) for img in img_list] if img_list is not None else None for img_list in images ] response = self.session.post( url, json={ "prompts": prompts, "images": images, "n": n, "repetition_penalty": repetition_penalty, "temperature": temperature, "top_p": top_p, "top_k": top_k, "min_p": min_p, "max_tokens": max_tokens, "logprobs": logprobs, "structured_outputs_regex": structured_outputs_regex, "generation_kwargs": generation_kwargs or {}, }, ) if response.status_code == 200: json_response = response.json() return { "prompt_ids": json_response["prompt_ids"], "completion_ids": json_response["completion_ids"], "logprobs": json_response["logprobs"], "logprob_token_ids": json_response["logprob_token_ids"], } else: raise Exception(f"Request failed: {response.status_code}, {response.text}") def chat( self, messages: list[list[dict]], n: int = 1, repetition_penalty: float = 1.0, temperature: float = 1.0, top_p: float = 1.0, top_k: int = 0, min_p: float = 0.0, max_tokens: int = 16, logprobs: int | None = 0, structured_outputs_regex: str | None = None, generation_kwargs: dict | None = None, chat_template_kwargs: dict | None = None, tools: list | None = None, chat_template: str | None = None, ) -> dict[str, list[list[int]]]: """ Generates model completions for the provided chat messages. Args: messages (`list[list[dict]]`): List of message lists for which the model will generate completions. Each message is a dictionary with keys like "role" and "content". n (`int`, *optional*, defaults to `1`): Number of completions to generate for each message list. repetition_penalty (`float`, *optional*, defaults to `1.0`): Parameter for repetition penalty. 1.0 means no penalty. temperature (`float`, *optional*, defaults to `1.0`): Temperature parameter for sampling. Higher values increase diversity. top_p (`float`, *optional*, defaults to `1.0`): Top-p sampling parameter.`1.0` means no truncation. top_k (`int`, *optional*, defaults to `0`): Top-k sampling parameter. `0` means no truncation. min_p (`float`, *optional*, defaults to `0.0`): Minimum probability for sampling. max_tokens (`int`, *optional*, defaults to `16`): Maximum number of tokens to generate for each message list. logprobs (`int` or `None`, *optional*, defaults to `0`): Number of top logprobs to return per token. When 0, only the sampled token's logprob is returned. When N>0, returns up to N+1 logprobs sorted by descending probability, because vLLM always includes the sampled token's logprob (which may fall outside the top-N). structured_outputs_regex (`str`, *optional*): Regular expression to guide the decoding process. generation_kwargs (`dict`, *optional*): Additional generation parameters to pass to the vLLM `SamplingParams`. This can include parameters like `seed`, `frequency_penalty`, etc. If it contains keys that conflict with the other parameters, they will override them. chat_template_kwargs (`dict`, *optional*): Additional keyword arguments to customize the chat template used by the model. tools (`list[dict | Callable]`, *optional*): List of tool functions available for tool calling during chat generation. chat_template (`str`, *optional*): Template to use for structuring the chat. If not provided, the model's default chat template will be used. Returns: `dict` with keys: - `prompt_ids` (`list[list[int]]`): List of lists of token IDs representing the tokenized input messages. - `completion_ids` (`list[list[int]]`): List of lists of token IDs representing the model-generated completions for each message list. - `logprobs` (`list[list[list[float]]]`): Per-token logprobs of shape (num_sequences, seq_len, num_logprobs), sorted by descending probability. - `logprob_token_ids` (`list[list[list[int]]]`): Token IDs corresponding to each logprob, same shape as `logprobs`. """ if chat_template is not None: raise NotImplementedError("Custom chat templates are not yet implemented in VLLMClient.chat().") url = f"{self.base_url}/chat/" # Convert PIL images to base64 strings messages = copy.deepcopy(messages) # avoid modifying the original messages for message_list in messages: for message in message_list: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image_pil": part["image_pil"] = pil_to_base64(part["image_pil"]) if isinstance(tools, list) and len(tools) > 0: tools = [get_json_schema(tool) if callable(tool) else tool for tool in tools] response = self.session.post( url, json={ "messages": messages, "n": n, "repetition_penalty": repetition_penalty, "temperature": temperature, "top_p": top_p, "top_k": top_k, "min_p": min_p, "max_tokens": max_tokens, "logprobs": logprobs, "structured_outputs_regex": structured_outputs_regex, "generation_kwargs": generation_kwargs or {}, "chat_template_kwargs": chat_template_kwargs or {}, "tools": tools, }, ) if response.status_code == 200: json_response = response.json() return { "prompt_ids": json_response["prompt_ids"], "completion_ids": json_response["completion_ids"], "logprobs": json_response["logprobs"], "logprob_token_ids": json_response["logprob_token_ids"], } else: raise Exception(f"Request failed: {response.status_code}, {response.text}") def init_communicator(self, device: torch.device | str | int = 0): """ Initializes the weight update group in a distributed setup for model synchronization. Args: device (`torch.device`, `str`, or `int`, *optional*, defaults to `0`): Device of trainer main process. It's the device that will be used for the weights synchronization. Can be a `torch.device` object, a string like `'cuda:0'`, or an integer device index. """ # Get the world size from the server url = f"{self.base_url}/get_world_size/" response = requests.get(url) if response.status_code == 200: vllm_world_size = response.json()["world_size"] else: raise Exception(f"Request failed: {response.status_code}, {response.text}") world_size = vllm_world_size + 1 # add the client to the world self.rank = vllm_world_size # the client's rank is the last process # Initialize weight update group url = f"{self.base_url}/init_communicator/" # Will simplify it after torch xpu 2.9 support get uuid. if is_torch_xpu_available(): if hasattr(torch.xpu.get_device_properties(device), "uuid"): client_device_uuid = str(torch.xpu.get_device_properties(device).uuid) else: client_device_uuid = "42" else: client_device_uuid = str(torch.cuda.get_device_properties(device).uuid) # Set the weight update group's host to "0.0.0.0" so that # clients from different IPs can send updated weights response = self.session.post( url, json={ "host": "0.0.0.0", "port": self.group_port, "world_size": world_size, "client_device_uuid": client_device_uuid, }, ) if response.status_code != 200: raise Exception(f"Request failed: {response.status_code}, {response.text}") # Brief delay to allow server initialization. While not strictly required (client socket will retry on # connection failure), this prevents log warnings like: # [W416 23:24:57.460001114 socket.cpp:204] [c10d] The hostname of the client socket cannot be retrieved. err=-3 time.sleep(0.1) # Set up the communication group for weight broadcasting if is_torch_xpu_available(): store = torch.distributed.TCPStore( host_name=self.host, port=self.group_port, world_size=world_size, is_master=(self.rank == 0) ) prefixed_store = c10d.PrefixStore("client2server", store) xccl_options = c10d.ProcessGroupXCCL.Options() pg = c10d.ProcessGroupXCCL( store=prefixed_store, rank=self.rank, size=world_size, options=xccl_options, ) self.communicator = pg else: pg = StatelessProcessGroup.create( host=self.host, port=self.group_port, rank=self.rank, world_size=world_size ) self.communicator = PyNcclCommunicator(pg, device=device) # When the client object is deleted, close the weight update group atexit.register(self.close_communicator) def update_named_param(self, name: str, weights: torch.Tensor): """ Updates a specific named parameter in the model and broadcasts it to other processes. Args: name (`str`): Name of the layer whose weights are being updated. weights (`torch.Tensor`): Tensor containing the updated weights. """ dtype, shape = str(weights.dtype), tuple(weights.shape) url = f"{self.base_url}/update_named_param/" response = self.session.post(url, json={"name": name, "dtype": dtype, "shape": shape}) if response.status_code != 200: raise Exception(f"Request failed: {response.status_code}, {response.text}") if is_torch_xpu_available(): # Use XCCL to broadcast the updated weights from the client (src) to all workers. self.communicator.broadcast(weights, root=self.rank) self.communicator.barrier() else: # Use NCCL to broadcast the updated weights from the client (src) to all workers. self.communicator.broadcast(weights, src=self.rank) self.communicator.group.barrier() def update_model_params(self, model: nn.Module): """ Updates all parameters of the given model by calling `update_named_param` for each parameter in the model. Args: model (`nn.Module`): Model whose parameters (weights/biases) are to be updated. """ for name, param in model.named_parameters(): # Update each parameter individually self.update_named_param(name, param.data) def reset_prefix_cache(self): """ Resets the prefix cache for the model. """ url = f"{self.base_url}/reset_prefix_cache/" response = self.session.post(url) if response.status_code != 200: raise Exception(f"Request failed: {response.status_code}, {response.text}") def close_communicator(self): """ Closes the weight update group and cleans up the communication group. """ url = f"{self.base_url}/close_communicator/" try: response = self.session.post(url) except ConnectionError: # The server might be already down, so we don't need to close the communicator pass else: if response.status_code != 200: raise Exception(f"Request failed: {response.status_code}, {response.text}") if self.communicator is not None: self.communicator = None # Example usage if __name__ == "__main__": from vllm import SamplingParams device = "xpu" if is_torch_xpu_available() else "cuda" client = VLLMClient() client.init_communicator(device=device) # Generate completions responses = client.generate(["Hello, AI!", "Tell me a joke"], n=4, max_tokens=32, sampling_params=SamplingParams()) print("Responses:", responses) # noqa # Update model weights from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B").to(device) client.update_model_params(model) ================================================ FILE: trl/generation/vllm_generation.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """vLLM-based generation backend for TRL trainers.""" import logging import math import os from contextlib import nullcontext from typing import TYPE_CHECKING import torch from accelerate.utils import broadcast_object_list, gather_object, is_peft_model from packaging.version import Version from torch import nn from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from transformers import PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, is_bitsandbytes_available from transformers.utils import is_torch_mlu_available, is_torch_npu_available, is_torch_xpu_available from ..extras.profiling import ProfilingContext from ..import_utils import is_vllm_available from ..trainer.utils import ensure_master_addr_port from .vllm_client import VLLMClient if is_vllm_available(): from vllm import LLM, RequestOutput, SamplingParams logger = logging.getLogger(__name__) def empty_cache() -> None: """Empties the cache of the available torch device. This function checks for the availability of different torch devices (XPU, MLU, NPU, CUDA) and empties the cache of the first available device it finds. If none of the specific devices are available, it defaults to emptying the CUDA cache. """ if is_torch_xpu_available(): torch.xpu.empty_cache() elif is_torch_mlu_available(): torch.mlu.empty_cache() elif is_torch_npu_available(): torch.npu.empty_cache() else: torch.cuda.empty_cache() def extract_logprobs(all_outputs: list["RequestOutput"]): """ Extract logprobs and token IDs from vLLM generation outputs. Returns logprobs and token IDs sorted by rank (most probable first). Each returned list has shape (num_sequences, seq_len, num_logprobs), where num_logprobs is determined by the `logprobs` parameter passed to vLLM (1 when `logprobs=0`, up to N+1 when `logprobs=N`). NaN logprob values are replaced with `None`. Args: all_outputs (list of `RequestOutput`): List of vLLM `RequestOutput` objects from generation. Returns: Tuple of (logprobs, logprob_token_ids), each of shape (num_sequences, seq_len, num_logprobs). """ all_logprobs = [] all_token_ids = [] for outputs in all_outputs: for output in outputs.outputs: if output.logprobs is None: return None, None seq_logprobs = [] seq_token_ids = [] for lp in output.logprobs: sorted_items = sorted(lp.items(), key=lambda x: x[1].rank) seq_token_ids.append([token_id for token_id, _ in sorted_items]) seq_logprobs.append([None if math.isnan(item.logprob) else item.logprob for _, item in sorted_items]) all_logprobs.append(seq_logprobs) all_token_ids.append(seq_token_ids) return all_logprobs, all_token_ids if TYPE_CHECKING: from accelerate import Accelerator from peft import PeftModel if is_bitsandbytes_available(): import bitsandbytes as bnb class VLLMGeneration: """Handles vLLM-based generation for trainers. Extracts all vLLM-specific logic (initialization, generation, weight sync) from trainers into a separate, testable class. Args: model ([`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to use for generation. accelerator ([`~accelerate.Accelerator`]): Accelerator for distributed training. is_fsdp_enabled (`bool`): Whether FSDP is enabled. processing_class ([`~transformers.PreTrainedTokenizerBase`] or [`~transformers.ProcessorMixin`]): Tokenizer or processor for the model. > Parameters for vLLM: mode (`str`, *optional*, defaults to `"colocate"`): vLLM mode. Must be one of `"colocate"` or `"server"`. - `"colocate"`: vLLM will run in the same process and share the training GPUs. This avoids the need for a separate server but may cause resource contention with training. - `"server"`: The trainer will send generation requests to a separate vLLM server. Make sure a TRL vLLM server is running (start with `trl vllm-serve`). structured_outputs_regex (`str`, *optional*): Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled. > Parameters for "server" vLLM mode: server_base_url (`str`, *optional*): Base URL for the vLLM server (e.g., `"http://localhost:8000"`). If provided, `server_host` and `server_port` are ignored. server_host (`str`, *optional*, defaults to `"0.0.0.0"`): Host of the vLLM server to connect to. Ignored if `server_base_url` is provided. server_port (`int`, *optional*, defaults to `8000`): Port of the vLLM server to connect to. Ignored if `server_base_url` is provided. server_timeout (`float`, *optional*, defaults to `240.0`): Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up after the timeout, a `ConnectionError` is raised. group_port (`int`, *optional*, defaults to `51216`): Port number for the weight update group. This is used to communicate with the vLLM server. Unless the port is occupied, there is no need to change it. > Parameters for "colocate" vLLM mode: tensor_parallel_size (`int`, *optional*, defaults to `1`): The number of GPUs to use for distributed execution with tensor parallelism. This setting only applies when `mode` is set to `"colocate"`. If you are using `mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_tensor_parallel_size` flag. gpu_memory_utilization (`float`, *optional*, defaults to `0.9`): Ratio (between 0 and 1) of GPU memory to reserve for the model weights, activations, and KV cache. Higher values will increase the KV cache size and thus improve the model's throughput. However, if the value is too high, it may cause out-of- memory (OOM) errors. This setting only applies when `mode` is set to `"colocate"`. If you are using `mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_gpu_memory_utilization` flag. max_model_length (`int`, *optional*): Model context length (prompt and completion). Set it to at least the maximum prompt length in the dataset plus `max_completion_length`; if omitted, it is inferred from the model config. max_num_seqs (`int`, *optional*): Maximum number of sequences to process in parallel, effectively capping the batch size. enable_sleep_mode (`bool`, *optional*, defaults to `False`): Whether to enable sleep mode for the engine to offload weights/cache during the optimizer step. Keeps GPU memory usage low, but waking the engine adds host–device transfer latency. model_impl (`str`, *optional*, defaults to `"auto"`): Model implementation to use for vLLM. - "auto" will try to use the vLLM implementation, if it exists, and fall back to the Transformers implementation if no vLLM implementation is available. - "vllm" will use the vLLM model implementation. - "transformers" will use the Transformers model implementation. - "terratorch" will use the TerraTorch model implementation. > Parameters for generation: repetition_penalty (`float`, *optional*, defaults to `1.0`): Parameter for repetition penalty. It penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > 1 encourage the model to use new tokens, while values < 1 encourage the model to repeat tokens. Default `1.0` means no penalty. temperature(`float`, *optional*, defaults to `1.0`): Sampling temperature. It controls the randomness of the sampling. Lower values make the model more deterministic, while higher values make the model more random and increase diversity. top_p: (`float`, *optional*, defaults to `1.0`): Top-p sampling parameter. It controls the cumulative probability of the top tokens to consider. Defaults to `1.0` to consider all tokens. top_k (`int`, *optional*, defaults to `0`): Top-k sampling parameter. It controls the number of top tokens to consider. Defaults to `0` to consider all tokens. min_p (`float`, *optional*, defaults to `0.0`): Min-p sampling parameter. It represents the minimum probability for a token to be considered, relative to the probability of the most likely token. Default `0.0` means min-p is disabled. max_completion_length (`int`, *optional*, defaults to `16`): Maximum number of tokens to generate for each prompt. logprobs (`int` or `None`, *optional*, defaults to `0`): Number of top logprobs to return per token. When 0 (default), only the sampled token's logprob is returned (inner dimension = 1). When N>0, returns up to N+1 logprobs sorted by descending probability, because vLLM always includes the sampled token's logprob alongside the top-N (the sampled token may or may not already be in the top-N). generation_kwargs (`dict`, *optional*): Additional generation parameters to pass to the vLLM `SamplingParams`. This can include parameters like `seed`, `frequency_penalty`, etc. If it contains keys that conflict with the other parameters, they will override them. > Parameters for chat/tools: chat_template (`str`, *optional*): Template to use for structuring the chat. If not provided, the model's default chat template will be used. chat_template_kwargs (`dict`, *optional*): Additional keyword arguments to customize the chat template used by the model. tools (`list`, *optional*): Tools available for tool calling during chat generation. """ def __init__( self, model: "PreTrainedModel | PeftModel", accelerator: "Accelerator", is_fsdp_enabled: bool, processing_class: PreTrainedTokenizerBase | ProcessorMixin, # vLLM configuration mode: str = "colocate", structured_outputs_regex: str | None = None, # Server mode configuration server_base_url: str | None = None, server_host: str = "0.0.0.0", server_port: int = 8000, server_timeout: float = 240.0, group_port: int = 51216, # Colocate mode configuration tensor_parallel_size: int = 1, gpu_memory_utilization: float = 0.9, max_model_length: int | None = None, max_num_seqs: int | None = None, enable_sleep_mode: bool = False, model_impl: str = "auto", # Generation configuration repetition_penalty: float = 1.0, temperature: float = 1.0, top_p: float = 1.0, top_k: int = 0, min_p: float = 0.0, max_completion_length: int = 16, logprobs: int | None = 0, generation_kwargs: dict | None = None, ): self.model = model self.accelerator = accelerator self.is_fsdp_enabled = is_fsdp_enabled self.processing_class = processing_class # vLLM configuration self.mode = mode self.structured_outputs_regex = structured_outputs_regex # Server mode configuration self.server_base_url = server_base_url self.server_host = server_host self.server_port = server_port self.group_port = group_port self.server_timeout = server_timeout # Colocate mode configuration self.tensor_parallel_size = tensor_parallel_size self.gpu_memory_utilization = gpu_memory_utilization self.max_model_length = max_model_length self.max_num_seqs = max_num_seqs self.enable_sleep_mode = enable_sleep_mode self.model_impl = model_impl # Generation configuration self.repetition_penalty = repetition_penalty self.temperature = temperature self.top_p = top_p self.top_k = top_k self.min_p = min_p self.max_completion_length = max_completion_length self.logprobs = logprobs self.generation_kwargs = generation_kwargs or {} self._init_vllm() def _init_vllm(self): """Initialize vLLM in server or colocate mode.""" model = self.model accelerator = self.accelerator if not is_vllm_available(): raise ImportError( "vLLM is not available and `use_vllm` is set to True. Please install vLLM with " "`pip install trl[vllm]` to use it." ) if self.mode == "server": if accelerator.is_main_process: if self.server_base_url is not None: base_url = self.server_base_url else: base_url = f"http://{self.server_host}:{self.server_port}" self.vllm_client = VLLMClient( base_url=base_url, group_port=self.group_port, connection_timeout=self.server_timeout ) self.vllm_client.init_communicator(device=torch.cuda.current_device()) elif self.mode == "colocate": # Make sure tensor_parallel_size group size evenly divides the world size - each group should have # the same number of ranks if not accelerator.num_processes % self.tensor_parallel_size == 0: raise ValueError( f"tensor_parallel_size ({self.tensor_parallel_size}) must divide world size " f"({accelerator.num_processes}) evenly." ) if self.tensor_parallel_size > 1: # Create subgroups of ranks for TP, each group with `tensor_parallel_size` ranks. # For example, if world_size=8 and tensor_parallel_size=2 → groups: [0,1], [2,3], [4,5], [6,7] self.tp_group, _ = torch.distributed.new_subgroups_by_enumeration( [ list(range(i * self.tensor_parallel_size, (i + 1) * self.tensor_parallel_size)) for i in range(accelerator.num_processes // self.tensor_parallel_size) ] ) # vLLM requires the environment variables to be set for distributed training. os.environ["RANK"] = str(accelerator.process_index) os.environ["LOCAL_RANK"] = str(accelerator.local_process_index) os.environ["WORLD_SIZE"] = str(accelerator.num_processes) # Ensure distributed rendezvous variables are set without colliding across concurrent runs ensure_master_addr_port() quantization = None if is_bitsandbytes_available(): for _, module in model.named_modules(): if isinstance(module, bnb.nn.Linear4bit): quantization = "bitsandbytes" break elif isinstance(module, bnb.nn.Linear8bitLt): raise ValueError("vLLM does not support in-flight 8-bit quantization.") # Build LLM initialization kwargs self.llm = LLM( model=model.name_or_path, tensor_parallel_size=self.tensor_parallel_size, gpu_memory_utilization=self.gpu_memory_utilization, max_model_len=self.max_model_length, max_num_seqs=self.max_num_seqs, enable_sleep_mode=self.enable_sleep_mode, model_impl=self.model_impl, distributed_executor_backend="external_launcher", # Feed identical seed for tp groups to ensure sampling results are the same across workers seed=accelerator.process_index // self.tensor_parallel_size, # Latest vLLM v1 memory profiler is misled by the high default value (i.e., 32768) - thinking there's not enough memory max_num_batched_tokens=4096, # Important so temperature scaling/logit tweaking affects the TIS log probs logprobs_mode="processed_logprobs", quantization=quantization, ) if self.enable_sleep_mode: self.llm.sleep(level=2) else: raise ValueError(f"vllm_mode must be either 'server' or 'colocate', got '{self.mode}'.") # When using vLLM, the main process is responsible for loading the model weights. This can cause process # desynchronization and seems to lead to DeepSpeed hanging during initialization. To prevent this, we # synchronize all processes after vLLM has been fully initialized. accelerator.wait_for_everyone() def _fix_param_name_to_vllm(self, name: str, extra_prefixes: list[str] | None = None) -> str: """Fix parameter name for vLLM compatibility.""" extra_prefixes = extra_prefixes or [] prefixes = ["_checkpoint_wrapped_module."] + extra_prefixes for prefix in prefixes: name = name.replace(prefix, "") return name def _sync_fsdp1_params_to_vllm(self, module: nn.Module, prefix: str = "", visited: set[str] | None = None): """Memory-efficient post-order traversal of FSDP modules to extract full parameters and sync with vLLM.""" # For FSDP1, we need to recurse into children and also use summon_full_params accelerator = self.accelerator if visited is None: visited = set() for child_name, child_module in module.named_children(): child_prefix = f"{prefix}.{child_name}" if prefix else child_name self._sync_fsdp1_params_to_vllm( child_module, prefix=child_prefix, visited=visited ) # recurse into the child if isinstance(module, FSDP): with FSDP.summon_full_params(module, recurse=False, writeback=False): for param_name, param in module.named_parameters(): full_name = f"{prefix}.{param_name}" if prefix else param_name full_name = self._fix_param_name_to_vllm(full_name, extra_prefixes=["_fsdp_wrapped_module."]) if full_name in visited: continue # skip FSDP subtrees already traversed visited.add(full_name) if self.mode == "server" and accelerator.is_main_process: self.vllm_client.update_named_param(full_name, param.data) elif self.mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(full_name, param.data)]) def _sync_fsdp2_params_to_vllm(self, module: nn.Module): """FSDP2-specific parameter synchronization.""" accelerator = self.accelerator # For FSDP2, module.state_dict() already covers all parameters, so no need for recursion for name, param in module.state_dict().items(): # When using PEFT, we need to recover the original parameter name name = name.removeprefix("base_model.model.").replace(".base_layer", "") # Skip PEFT layers: they don't exist in vLLM, and they are merged already. if is_peft_model(module) and module.prefix in name: continue # When module to save, remove its prefix and discard the original module if "original_module" in name: continue name = self._fix_param_name_to_vllm(name, extra_prefixes=["modules_to_save.default."]) if param.is_cpu: param = param.to(torch.device("cuda")) param = param.full_tensor() if self.mode == "server" and accelerator.is_main_process: self.vllm_client.update_named_param(name, param) elif self.mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param)]) def sync_weights(self): """Synchronize model weights to vLLM. Handles FSDP, DeepSpeed, PEFT weight synchronization. """ # Wake up vLLM weights before loading to ensure device memory is mapped. Without this, load_weights() writes to # freed/unmapped memory when sleep mode is active, which crashes on backends with strict physical memory # management (e.g., Ascend NPU). See https://github.com/huggingface/trl/issues/5142 if self.mode == "colocate" and self.enable_sleep_mode: empty_cache() # required to avoid OOM in some cases self.llm.wake_up(tags=["weights"]) model = self.model accelerator = self.accelerator is_fsdp_enabled = self.is_fsdp_enabled # For DeepSpeed ZeRO-3 and FSDP, we need to gather all parameters before operations deepspeed_plugin = accelerator.state.deepspeed_plugin zero_stage_3 = deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3 if zero_stage_3: import deepspeed gather_if_zero3 = deepspeed.zero.GatheredParameters else: gather_if_zero3 = nullcontext if is_peft_model(model): # With PEFT and FSDP/DeepSpeed ZeRO Stage 3, we must gather the full model at once before merging, as # merging adapters in a sharded manner is not supported. # TODO: does this work with FSDP? with gather_if_zero3(list(model.parameters())): model.merge_adapter() # Update vLLM weights while parameters are gathered if is_fsdp_enabled: # note if using FSDP, gather_if_zero3 is nullcontext # Update vLLM weights while parameters are gathered # For PEFT with FSDP we need to use the memory efficient post-order traversal fsdp_plugin = getattr(accelerator.state, "fsdp_plugin", None) fsdp_version = getattr(fsdp_plugin, "fsdp_version", 1) if fsdp_plugin else 1 if fsdp_version == 1: self._sync_fsdp1_params_to_vllm(model) # use memory-efficient post-order traversal for FSDP elif fsdp_version == 2: self._sync_fsdp2_params_to_vllm(model) else: # DeepSpeed ZeRO-3 with PEFT for name, param in model.named_parameters(): # When using PEFT, we need to recover the original parameter name name = name.removeprefix("base_model.model.").replace(".base_layer", "") # Skip PEFT layers: they don't exist in vLLM, and they are merged already. if model.prefix in name: continue # When module to save, remove its prefix and discard the original module if "original_module" in name: continue name = self._fix_param_name_to_vllm(name, extra_prefixes=["modules_to_save.default."]) if self.mode == "server" and accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Unmerge adapters while parameters are still gathered model.unmerge_adapter() # Parameters will automatically be repartitioned when exiting the context else: # For non-PEFT models, simply gather (if needed) and update each parameter individually. if is_fsdp_enabled: fsdp_plugin = getattr(accelerator.state, "fsdp_plugin", None) fsdp_version = getattr(fsdp_plugin, "fsdp_version", 1) if fsdp_plugin else 1 if fsdp_version == 1: self._sync_fsdp1_params_to_vllm(model) # use memory-efficient post-order traversal for FSDP elif fsdp_version == 2: self._sync_fsdp2_params_to_vllm(model) else: for name, param in model.named_parameters(): name = self._fix_param_name_to_vllm(name) with gather_if_zero3([param]): if self.mode == "server" and accelerator.is_main_process: self.vllm_client.update_named_param(name, param.data) elif self.mode == "colocate": llm_model = self.llm.llm_engine.model_executor.driver_worker.model_runner.model llm_model.load_weights([(name, param.data)]) # Reset cache on vLLM if self.mode == "server" and accelerator.is_main_process: self.vllm_client.reset_prefix_cache() elif self.mode == "colocate": self.llm.reset_prefix_cache() def generate( self, prompts: list[list[int]], images: list[list | None] | None, num_generations: int, profiler: ProfilingContext | None = None, ) -> tuple: """Generate completions using vLLM. Args: prompts: List of token ID lists, one per prompt (already tokenized). images: Optional list of image lists for VLM support. Each element is a list of PIL images for the corresponding prompt, or `None` if no images for that prompt. `None` if no images at all. num_generations: Number of generations per prompt. profiler: Optional profiler for performance tracking. Returns: Tuple of (prompt_ids, completion_ids, logprobs, logprob_token_ids). - `prompt_ids`: `list[list[int]]` of shape `(batch_size, prompt_len)`. - `completion_ids`: `list[list[int]]` of shape `(batch_size, completion_len)`. - `logprobs`: `list[list[list[float | None]]]` of shape `(batch_size, completion_len, num_logprobs)`. - `logprob_token_ids`: `list[list[list[int]]]` of shape `(batch_size, completion_len, num_logprobs)`. `num_logprobs` is 1 when `logprobs=0`, or up to N+1 when `logprobs=N` (the sampled token is always included and may fall outside the top-N). """ import vllm if Version(vllm.__version__) <= Version("0.10.2"): from vllm.sampling_params import GuidedDecodingParams as StructuredOutputsParams structured_outputs_key = "guided_decoding" else: from vllm.sampling_params import StructuredOutputsParams structured_outputs_key = "structured_outputs" profiler = profiler or nullcontext() accelerator = self.accelerator temperature = self.temperature top_p = self.top_p top_k = self.top_k min_p = self.min_p repetition_penalty = self.repetition_penalty max_completion_length = self.max_completion_length # Wake up colocated vLLM weights if needed (idempotent if already awake from sync_weights) if self.mode == "colocate" and self.enable_sleep_mode: empty_cache() # required to avoid OOM in some cases self.llm.wake_up(tags=["weights"]) # Work around for https://github.com/vllm-project/vllm/issues/29341 try: self.llm.collective_rpc("reload_weights") except NotImplementedError: # Non-CUDA vLLM backends (e.g., vllm-ascend's NPUWorkerV1), don't implement reload_weights pass # Generate completions using vLLM: gather all prompts and use them in a single call in the main process if self.mode == "server": all_prompts = gather_object(prompts) # Always gather images (even when None) to avoid deadlock: images may be None on some ranks # and non-None on others in mixed datasets, and gather_object is a collective operation. all_images = gather_object(images if images is not None else [None] * len(prompts)) if all(img is None for img in all_images): all_images = None if accelerator.is_main_process: # Since 'prompts' contains 'num_generations' duplicates, we first take unique prompts, and # generate num_generations outputs for each one. This is faster than generating outputs for each # duplicate prompt individually. ordered_set_of_prompt_ids = all_prompts[::num_generations] ordered_set_of_images = all_images[::num_generations] if all_images is not None else None sampling_params = { "n": num_generations, "repetition_penalty": repetition_penalty, "temperature": temperature, "top_p": top_p, "top_k": top_k, "min_p": 0.0 if min_p is None else min_p, "max_tokens": max_completion_length, "logprobs": self.logprobs, "structured_outputs_regex": self.structured_outputs_regex, "generation_kwargs": self.generation_kwargs, } with profiler: output = self.vllm_client.generate( prompts=ordered_set_of_prompt_ids, images=ordered_set_of_images, **sampling_params, ) payload = ( output["prompt_ids"], output["completion_ids"], output["logprobs"], output.get("logprob_token_ids"), ) else: payload = None # Broadcast the completions from the main process to all processes, ensuring each process receives its corresponding slice. obj_list = [payload] broadcast_object_list(obj_list, from_process=0) all_prompt_ids, all_completion_ids, all_logprobs, all_logprob_token_ids = obj_list[0] # vllm_client.generate(n=num_generations) returns num_generations completions per prompt. # Duplicate prompt_ids to align with per-completion entries. all_prompt_ids = [ids for ids in all_prompt_ids for _ in range(num_generations)] process_slice = slice( accelerator.process_index * len(prompts), (accelerator.process_index + 1) * len(prompts), ) prompt_ids = all_prompt_ids[process_slice] completion_ids = all_completion_ids[process_slice] logprobs = all_logprobs[process_slice] if all_logprobs is not None else None logprob_token_ids = all_logprob_token_ids[process_slice] if all_logprob_token_ids is not None else None # Generate completions using colocated vLLM instances: each device holds vLLM copy and work on their own batch of prompts elif self.mode == "colocate": generation_kwargs = { "n": 1, # vLLM on each GPU generates only 1 in colocate mode "repetition_penalty": repetition_penalty, "temperature": temperature, "top_p": top_p, "top_k": top_k, "min_p": 0.0 if min_p is None else min_p, "max_tokens": max_completion_length, "logprobs": self.logprobs, } generation_kwargs.update(self.generation_kwargs) if self.structured_outputs_regex is not None: if generation_kwargs.get(structured_outputs_key) is not None: logger.warning( f"Both `structured_outputs_regex` and `generation_kwargs['{structured_outputs_key}']` are set; " "`structured_outputs_regex` takes precedence." ) generation_kwargs[structured_outputs_key] = StructuredOutputsParams( regex=self.structured_outputs_regex ) elif isinstance(structured_outputs_kwargs := generation_kwargs.get(structured_outputs_key), dict): generation_kwargs[structured_outputs_key] = StructuredOutputsParams(**structured_outputs_kwargs) sampling_params = SamplingParams(**generation_kwargs) if self.tensor_parallel_size > 1: # Gather prompts from all ranks in the TP group and flatten. # Each rank starts with its own prompts; after gathering, all ranks see the full group set. orig_size = len(prompts) gathered_prompts = [None for _ in range(self.tensor_parallel_size)] torch.distributed.all_gather_object(gathered_prompts, prompts, group=self.tp_group) all_prompts = [p for sublist in gathered_prompts for p in sublist] # Always gather images (even when None) to avoid deadlock: images may be None on some # ranks and non-None on others in mixed datasets, and all_gather_object is collective. local_images = images if images is not None else [None] * len(prompts) gathered_images = [None for _ in range(self.tensor_parallel_size)] torch.distributed.all_gather_object(gathered_images, local_images, group=self.tp_group) all_images = [img for sublist in gathered_images for img in sublist] if all(img is None for img in all_images): all_images = None else: all_prompts = prompts all_images = images if self.enable_sleep_mode: self.llm.wake_up(tags=["kv_cache"]) # Build vLLM-compatible prompt inputs with token IDs and optional multi-modal data vllm_prompts = [] if all_images is not None: for ids, img_list in zip(all_prompts, all_images, strict=True): row = {"prompt_token_ids": ids} if img_list is not None: row["multi_modal_data"] = {"image": img_list if len(img_list) > 1 else img_list[0]} vllm_prompts.append(row) else: vllm_prompts = [{"prompt_token_ids": ids} for ids in all_prompts] with profiler: all_outputs = self.llm.generate(vllm_prompts, sampling_params=sampling_params, use_tqdm=False) all_prompt_ids = [output.prompt_token_ids for output in all_outputs] all_completion_ids = [output.token_ids for outputs in all_outputs for output in outputs.outputs] all_logprobs, all_logprob_token_ids = extract_logprobs(all_outputs) if self.tensor_parallel_size > 1: # Slice completions for this rank within its TP group. # Each rank generates all outputs — we keep only our share. local_rank_in_group = torch.distributed.get_rank(group=self.tp_group) tp_slice = slice(local_rank_in_group * orig_size, (local_rank_in_group + 1) * orig_size) prompt_ids = all_prompt_ids[tp_slice] completion_ids = all_completion_ids[tp_slice] logprobs = all_logprobs[tp_slice] if all_logprobs is not None else None logprob_token_ids = all_logprob_token_ids[tp_slice] if all_logprob_token_ids is not None else None else: prompt_ids = all_prompt_ids completion_ids = all_completion_ids logprobs = all_logprobs logprob_token_ids = all_logprob_token_ids if self.enable_sleep_mode: self.llm.sleep(level=2) return prompt_ids, completion_ids, logprobs, logprob_token_ids ================================================ FILE: trl/import_utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import importlib import importlib.metadata import importlib.util import warnings from contextlib import contextmanager from packaging.version import Version LIGER_KERNEL_MIN_VERSION = "0.7.0" PACKAGE_DISTRIBUTION_MAPPING = importlib.metadata.packages_distributions() # From transformers: https://github.com/huggingface/transformers/blob/556312cd45a5e619c41b0f8adf680eab0d334324/src/transformers/utils/import_utils.py#L48-L77 def _is_package_available(pkg_name: str, return_version: bool = False) -> tuple[bool, str] | bool: """Check if `pkg_name` exist, and optionally try to get its version""" spec = importlib.util.find_spec(pkg_name) package_exists = spec is not None package_version = "N/A" if package_exists and return_version: try: # importlib.metadata works with the distribution package, which may be different from the import # name (e.g. `PIL` is the import name, but `pillow` is the distribution name) distributions = PACKAGE_DISTRIBUTION_MAPPING[pkg_name] # Per PEP 503, underscores and hyphens are equivalent in package names. # Prefer the distribution that matches the (normalized) package name. normalized_pkg_name = pkg_name.replace("_", "-") if normalized_pkg_name in distributions: distribution_name = normalized_pkg_name elif pkg_name in distributions: distribution_name = pkg_name else: distribution_name = distributions[0] package_version = importlib.metadata.version(distribution_name) except (importlib.metadata.PackageNotFoundError, KeyError): # If we cannot find the metadata (because of editable install for example), try to import directly. # Note that this branch will almost never be run, so we do not import packages for nothing here package = importlib.import_module(pkg_name) package_version = getattr(package, "__version__", "N/A") if return_version: return package_exists, package_version else: return package_exists def is_deepspeed_available() -> bool: return _is_package_available("deepspeed") def is_fastapi_available() -> bool: return _is_package_available("fastapi") def is_jmespath_available() -> bool: return _is_package_available("jmespath") def is_joblib_available() -> bool: return _is_package_available("joblib") def is_liger_kernel_available(min_version: str = LIGER_KERNEL_MIN_VERSION) -> bool: _liger_kernel_available, _liger_kernel_version = _is_package_available("liger_kernel", return_version=True) return _liger_kernel_available and Version(_liger_kernel_version) >= Version(min_version) def is_llm_blender_available() -> bool: return _is_package_available("llm_blender") def is_math_verify_available() -> bool: return _is_package_available("math_verify") def is_mergekit_available() -> bool: return _is_package_available("mergekit") def is_pydantic_available() -> bool: return _is_package_available("pydantic") def is_requests_available() -> bool: return _is_package_available("requests") def is_unsloth_available() -> bool: return _is_package_available("unsloth") def is_uvicorn_available() -> bool: return _is_package_available("uvicorn") def is_vllm_available(min_version: str | None = None) -> bool: _vllm_available, _vllm_version = _is_package_available("vllm", return_version=True) if _vllm_available: if not (Version("0.10.2") <= Version(_vllm_version) <= Version("0.17.1")): warnings.warn( f"TRL currently supports vLLM versions from 0.10.2 to 0.17.1. You have version {_vllm_version} " "installed. We recommend installing a supported version to avoid compatibility issues.", stacklevel=2, ) if min_version is not None and Version(_vllm_version) < Version(min_version): return False return _vllm_available def is_vllm_ascend_available() -> bool: return _is_package_available("vllm_ascend") def is_weave_available() -> bool: return _is_package_available("weave") class TRLExperimentalWarning(UserWarning): """Warning for using the 'trl.experimental' submodule.""" pass @contextmanager def suppress_warning(category): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=category) yield def suppress_experimental_warning(): return suppress_warning(TRLExperimentalWarning) ================================================ FILE: trl/models/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import TYPE_CHECKING from .._lazy_module import _LazyModule _import_structure = { "activation_offloading": ["get_act_offloading_ctx_manager"], "utils": ["create_reference_model", "prepare_deepspeed", "prepare_fsdp", "unwrap_model_for_generation"], } if TYPE_CHECKING: from .activation_offloading import get_act_offloading_ctx_manager from .utils import create_reference_model, prepare_deepspeed, prepare_fsdp, unwrap_model_for_generation else: import sys sys.modules[__name__] = _LazyModule(__name__, globals()["__file__"], _import_structure, module_spec=__spec__) ================================================ FILE: trl/models/activation_offloading.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of https://github.com/pytorch/torchtune. import psutil import torch from accelerate import logging from accelerate.utils.versions import is_torch_version from torch import nn from torch.autograd.graph import saved_tensors_hooks from transformers import is_torch_npu_available if is_torch_npu_available(): import torch_npu # noqa: F401 # Import DTensor for FSDP v2 support with version-aware import path DTensor = None if torch.distributed.is_available(): try: if is_torch_version(">=", "2.5.0"): from torch.distributed.tensor import DTensor else: # from torch 2.0.0 (oldest supported accelerate torch version), DTensor is in torch.distributed._tensor from torch.distributed._tensor import DTensor except (ImportError, AttributeError): DTensor = None logger = logging.get_logger(__name__) def _get_unique_tensor_key(tensor: torch.Tensor) -> tuple: """ Get a unique key for a tensor based on its storage pointer and dtype. This allows deduplication of tensors that share the same underlying storage. From: https://github.com/volcengine/verl/blob/main/verl/utils/activation_offload.py Args: tensor: The tensor to get the key for Returns: A tuple of (storage_pointer, dtype) that uniquely identifies the tensor's storage """ # Handle special tensor types - primarily for FSDP v2 DTensor actual_tensor = tensor # For DTensor (FSDP v2), extract the local tensor if DTensor is not None and isinstance(tensor, DTensor) and hasattr(tensor, "_local_tensor"): actual_tensor = tensor._local_tensor # Try to get storage pointer, but fall back to tensor id if not accessible try: storage_ptr = actual_tensor.untyped_storage().data_ptr() + actual_tensor.storage_offset() except (RuntimeError, AttributeError): # For tensors with invalid storage, use tensor id # This won't enable deduplication for these tensors, but allows offloading to work storage_ptr = id(actual_tensor) return (storage_ptr, actual_tensor.dtype) class OffloadActivations(saved_tensors_hooks): """ Context manager under which activation tensors created in the forward pass will be offloaded. Enable the memory efficiency technique of activation offloading, where activations bigger than `min_offload_size` bytes will be offloaded to CPU in the forward and brought back in the backward. This is in contrast to maintaining the activation on GPU VRAM throughout the program. This manager contains the option of using one additional CUDA stream to handle the communication between CUDA and CPU, which is intended to overlap with the default computation stream to improve runtime. We designed synchronization with a few heuristics for optimizing the tradeoff between runtime vs memory usage. Args: use_pin_memory (`bool`, *optional*, defaults to `True`): Whether to offloaded Tensor will be placed in pinned memory on the CPU. Pinned memory allows the Tensor to be moved back onto GPU more quickly but is a limited resource. use_streams (`bool`, *optional*, defaults to `True`): Whether to use streams for performance optimization where the communications get overlapped with the computation. Requires a torch build after torch-2.5.0. min_offload_size (`int`, *optional*, defaults to `1024`): Minimum number of bytes a Tensor must be in order to qualify for offloading. If the tensor is too small, we do not want to waste bandwidth and resources moving it to CPU and back. max_fwd_stash_size (`int`, *optional*, defaults to `5`): Maximum size of the forward stash, or the maximum number of consecutive activations to keep alive during the forward pass. This number must be at least 1. Keeping alive more activations will potentially allow more overlap between the communication and compute streams at the cost of increasing memory usage. Keeping alive fewer activations will conserve memory, but may cause poor overlap between the streams, increasing runtime. Raises: ValueError: if `max_fwd_stash_size` is not at least `1`. Example: ```python >>> with OffloadActivations(): ... outputs = model(inputs, labels=labels) >>> loss = outputs.loss >>> loss.backward() ``` """ def __init__( self, use_pin_memory: bool = True, use_streams: bool = True, min_offload_size: int = 1024, max_fwd_stash_size: int = 5, ) -> None: self.use_streams = use_streams self.min_tensor_size_bytes = min_offload_size # we don't want to bother with small tensors self.tracker = {} # tensor_id => (new_tensor, if_modified) ---> track what saved/offloaded tensors are where self.tensor_id = 0 self.is_first_forward_call = True self.is_first_backward_call = True self.is_first_forward_pass = True # Storage deduplication: maps storage key to tensor_id to avoid offloading same storage multiple times self.storage_to_tensor_id = {} # Parameter filtering: track parameter storage pointers to skip them during offloading self.param_storages = set() # Managing cpu memory self.use_pin_memory = use_pin_memory self.virtual_memory_safe_pct = 60 # we should not exceed this percentage of memory self.accelerator_type = ( torch.accelerator.current_accelerator().type if hasattr(torch, "accelerator") else "cuda" ) # NOTE: xpu doesn't have `default_stream` API, use `current_stream` instead if self.accelerator_type == "xpu": # comp stream self.s0 = torch.xpu.current_stream() elif is_torch_npu_available() and self.accelerator_type == "npu": self.s0 = torch.npu.current_stream() else: self.s0 = torch.cuda.default_stream() # For streaming if self.use_streams: if self.accelerator_type == "xpu": # comms stream self.s1 = torch.xpu.Stream() elif self.accelerator_type == "npu": self.s1 = torch.npu.Stream() else: self.s1 = torch.cuda.Stream() self.fwd_stash = {} # tensor_id => (activation, ev1) if max_fwd_stash_size < 1: raise ValueError(f"max_fwd_stash_size should be at least 1 but is {max_fwd_stash_size}") self.max_fwd_stash_size = max_fwd_stash_size self.bwd_tensor_stash = {} # tensor_id => activation self.bwd_ev_stash = {} # tensor_id => ev0 self.curr_graph_id = None self.curr_autograd_node = None # -------- platform util functions -------- # def verify_sufficient_virtual_memory(): curr_pct = get_cpu_ram_pct() if curr_pct > self.virtual_memory_safe_pct: logger.warning(f"{curr_pct=}% > {self.virtual_memory_safe_pct=}% of virtual memory used") def get_cpu_ram_pct() -> float: # get the percentage of memory used by the system return psutil.virtual_memory().percent def get_tensor_id() -> int: # create a unique id for each tensor we are managing self.tensor_id += 1 return self.tensor_id def get_num_bytes_tensor(x: torch.Tensor) -> int: # get the number of bytes in a tensor, for memory management purposes return x.element_size() * x.nelement() # x.element_size() * x._base_storage().nbytes() # -------- core pack / unpack work -------- # def pack_tensor(activation: torch.Tensor) -> int: # activations are passed in during forward pass - from here we take over and return a unique id if self.is_first_forward_call: if len(self.tracker) != 0: raise ValueError("Backward pass should have cleared tracker of all tensors") # set training phase trackers self.is_first_forward_call = False self.is_first_backward_call = True # Reset deduplication map for new forward pass self.storage_to_tensor_id = {} # query for basic tensor info num_bytes = get_num_bytes_tensor(activation) tensor_id = get_tensor_id() # Check for tensor deduplication using storage pointer # If this storage is already being tracked, we still create a new tensor_id # but don't offload again (just keep the tensor in GPU) storage_key = _get_unique_tensor_key(activation) if storage_key in self.storage_to_tensor_id: # Storage already offloaded - don't offload again, just track the reference self.tracker[tensor_id] = (activation, False, None, None, None) # Keep on GPU, don't offload return tensor_id # Check if tensor is on CPU (skip offloading) if activation.device.type not in ["cuda", "xpu", "npu"]: self.tracker[tensor_id] = (activation, False, None, None, None) return tensor_id # Check if tensor is too small if num_bytes < self.min_tensor_size_bytes: self.tracker[tensor_id] = (activation, False, None, None, None) return tensor_id # Check if tensor is a parameter or buffer if isinstance(activation, torch.nn.Parameter) or ( hasattr(torch.nn, "Buffer") and isinstance(activation, torch.nn.Buffer) ): self.tracker[tensor_id] = (activation, False, None, None, None) return tensor_id # Check if tensor is an FP8 tensor (TorchAO) - skip offloading as they're already compressed tensor_class_name = type(activation).__name__ if tensor_class_name in ["Float8TrainingTensor", "ScaledMMConfig", "LinearMMConfig"]: self.tracker[tensor_id] = (activation, False, None, None, None) return tensor_id # Check if tensor storage is a model parameter (for FSDP compatibility) try: # Extract actual tensor for DTensor check_tensor = activation if DTensor is not None and isinstance(activation, DTensor) and hasattr(activation, "_local_tensor"): check_tensor = activation._local_tensor if check_tensor.untyped_storage().data_ptr() in self.param_storages: self.tracker[tensor_id] = (activation, False, None, None, None) return tensor_id except (RuntimeError, AttributeError): # If we can't get data_ptr, skip this check pass # Tensor qualifies for offloading if self.use_streams: # First, sync back and dereference previously offloaded tensors # as the offloading should be done sufficiently long ago. for id in list(self.fwd_stash.keys()): if id <= tensor_id - self.max_fwd_stash_size: _, ev = self.fwd_stash[id] self.s0.wait_event(ev) del self.fwd_stash[id] else: break # Sync in, offload, and add an event to sync back later self.s1.wait_stream(self.s0) stream = self.s1 if self.use_streams else self.s0 if self.accelerator_type == "xpu": stream_ctx = torch.xpu.stream(stream) elif self.accelerator_type == "npu": stream_ctx = torch.npu.stream(stream) else: stream_ctx = torch.cuda.stream(stream) with stream_ctx: # Save original stride and shape information original_stride = activation.stride() original_storage_offset = activation.storage_offset() original_shape = activation.size() # Check if tensor has broadcast dimensions (stride == 0) # If so, copy the underlying storage directly instead of materializing the broadcast has_broadcast = 0 in original_stride if has_broadcast: # Copy only the actual underlying storage, not the materialized broadcast # Create CPU tensor with same storage size as original storage_size = activation.untyped_storage().size() cpu_storage = torch.empty( storage_size // activation.element_size(), dtype=activation.dtype, pin_memory=self.use_pin_memory, device="cpu", ) # Copy the raw storage cpu_storage_view = torch.as_strided( activation, size=(storage_size // activation.element_size(),), stride=(1,), storage_offset=0 ) cpu_storage.copy_(cpu_storage_view, non_blocking=True) cpu_tensor = cpu_storage else: # No broadcast - use normal contiguous copy cpu_tensor = torch.empty_like(activation, pin_memory=self.use_pin_memory, device="cpu") cpu_tensor.copy_(activation, non_blocking=True) # Store CPU tensor along with stride information self.tracker[tensor_id] = ( cpu_tensor, True, # True = (in future) modified original_stride, # Save original GPU stride original_storage_offset, # Save original storage offset original_shape, # Save original shape for broadcast restoration ) if self.use_streams: event = self.s1.record_event() # Stash to keep activation alive til s1 is done self.fwd_stash[tensor_id] = (activation, event) # Track this storage for deduplication self.storage_to_tensor_id[storage_key] = tensor_id return tensor_id def unpack_tensor_single_stream(unpack_tensor_id: int) -> torch.Tensor: # backward pass - we are called with the tensor_id, which # we will use to retrieve the saved/offloaded tensor if self.is_first_backward_call: if self.is_first_forward_pass: self.is_first_forward_pass = False if self.use_pin_memory: verify_sufficient_virtual_memory() self.is_first_backward_call = False if unpack_tensor_id not in self.tracker: raise ValueError(f"Untracked tensor with id {unpack_tensor_id}") ( maybe_accelerator_tensor, modified, original_stride, original_storage_offset, original_shape, ) = self.tracker[unpack_tensor_id] if modified: # Restore tensor to GPU accelerator_tensor = maybe_accelerator_tensor.to(self.accelerator_type, non_blocking=True) # Restore original stride if we saved it (handles both broadcast and non-broadcast cases) if original_stride is not None: accelerator_tensor = torch.as_strided( accelerator_tensor, size=original_shape, stride=original_stride, storage_offset=original_storage_offset, ) maybe_accelerator_tensor = accelerator_tensor # clear tensor from tracking del self.tracker[unpack_tensor_id] # Only set is_first_forward_call to True when all tensors have been unpacked if len(self.tracker) == 0: self.is_first_forward_call = True return maybe_accelerator_tensor def unpack_tensor_with_streams(unpack_tensor_id: int) -> torch.Tensor: # backward pass - we are called with the tensor_id, which # we will use to retrieve the saved/offloaded tensor if self.is_first_backward_call: self.curr_graph_id = torch._C._current_graph_task_id() def wait_and_del_remaining_references() -> None: for id in list(self.bwd_tensor_stash.keys()): if id in self.bwd_ev_stash: event = self.bwd_ev_stash[id] self.s1.wait_event(event) del self.bwd_tensor_stash[id] # Register a callback to the end of autograd to clean everything up torch.autograd.variable.Variable._execution_engine.queue_callback(wait_and_del_remaining_references) if self.is_first_forward_pass: self.is_first_forward_pass = False if self.use_pin_memory: verify_sufficient_virtual_memory() self.is_first_backward_call = False if unpack_tensor_id not in self.tracker: raise ValueError(f"untracked tensor with id {unpack_tensor_id}") ( maybe_accelerator_tensor, modified, original_stride, original_storage_offset, original_shape, ) = self.tracker[unpack_tensor_id] if modified: # Get data on the current autograd node graph_id = torch._C._current_graph_task_id() node = torch._C._current_autograd_node() prev_node_ids = [] # If we're on a new node, mark prev node's tensors to be freed later if graph_id == self.curr_graph_id and self.curr_autograd_node != node: self.curr_autograd_node = node prev_node_ids = list(self.bwd_tensor_stash.keys()) brought_back_from_cpu = True if unpack_tensor_id in self.fwd_stash: maybe_accelerator_tensor = self.fwd_stash[unpack_tensor_id][0] brought_back_from_cpu = False else: # Kick off the process to bring tensors back if self.accelerator_type == "xpu": stream_ctx = torch.xpu.stream(self.s1) elif self.accelerator_type == "npu": stream_ctx = torch.npu.stream(self.s1) else: stream_ctx = torch.cuda.stream(self.s1) with stream_ctx: # Restore tensor to GPU accelerator_tensor = maybe_accelerator_tensor.to(self.accelerator_type, non_blocking=True) # Restore original stride if we saved it (handles both broadcast and non-broadcast cases) if original_stride is not None: accelerator_tensor = torch.as_strided( accelerator_tensor, size=original_shape, stride=original_stride, storage_offset=original_storage_offset, ) maybe_accelerator_tensor = accelerator_tensor # Tell comp stream to wait for the info to be loaded before executing self.s0.wait_stream(self.s1) # Stash the tensor to keep memory alive until compute stream is complete self.bwd_tensor_stash[unpack_tensor_id] = maybe_accelerator_tensor # Note: [Track views of the unpacked] # Why do we get the use count of the unpacked tensor here? We want an # initial count to compare to later, during the post-hook of the # backward node, when we need to decide whether we're allowed to free # the tensor yet. In what obscure cases must we delay freeing the # tensor (and thus call record_stream)? # 1. Any of the outputs of the backward node is a view of the unpacked # tensor. # 2. In the case that this unpacked tensor will be used in a # checkpointed region, if one of the recomputed saved tensors ends # up as a view of the unpacked tensor. # 3. The user abuses the system somehow and manually relies on the # unpacked tensor to exist after the backward node has executed. if self.accelerator_type == "npu": storage_refcount = torch_npu._C._storage_Use_Count( maybe_accelerator_tensor.untyped_storage()._cdata ) else: storage_refcount = torch._C._storage_Use_Count( maybe_accelerator_tensor.untyped_storage()._cdata ) def hook(outputs, inputs): # create events for the current node inputs/outputs if they were streamed in if brought_back_from_cpu: # See Note: [Track views of the unpacked] # IF any of the outputs is a view of the tensor, OR if a view of # the tensor has been saved as a part of checkpoint's recompute # process, OR the user has abusedly incurred a reference on the # unpacked tensor, THEN the tensor might be used later and we # cannot presume to delete it after only the current node is # done! So we use our frenemy, record_stream, to ensure the # Tensor stays unmessed with until it's done getting used in the # compute stream (s0 here). Note that the con here is we introduce # non-deterministic (thus higher) memory usage, but this case # should not happen often. # Check if tensor still exists (might have been cleaned up by a previous node) if unpack_tensor_id in self.bwd_tensor_stash: unpacked_tensor = self.bwd_tensor_stash[unpack_tensor_id] if self.accelerator_type == "npu": storage_count = torch_npu._C._storage_Use_Count( unpacked_tensor.untyped_storage()._cdata ) else: storage_count = torch._C._storage_Use_Count(unpacked_tensor.untyped_storage()._cdata) if storage_count > storage_refcount: unpacked_tensor.record_stream(self.s0) del self.bwd_tensor_stash[unpack_tensor_id] else: event = self.s0.record_event() self.bwd_ev_stash[unpack_tensor_id] = event # if there are still things in the fwd_stash, get rid of them as we're in bwd now for id in list(self.fwd_stash.keys()): _, ev = self.fwd_stash[id] self.s0.wait_event(ev) del self.fwd_stash[id] # wait on prev node's events and del those for id in prev_node_ids: # Only wait on events that exist (some tensors may have used record_stream instead) if id in self.bwd_ev_stash: event = self.bwd_ev_stash[id] self.s1.wait_event(event) del self.bwd_ev_stash[id] if id in self.bwd_tensor_stash: del self.bwd_tensor_stash[id] return outputs node.register_hook(hook) # clear tensor from tracking del self.tracker[unpack_tensor_id] # Only set is_first_forward_call to True when all tensors have been unpacked if len(self.tracker) == 0: self.is_first_forward_call = True return maybe_accelerator_tensor unpack_tensor = unpack_tensor_with_streams if self.use_streams else unpack_tensor_single_stream super().__init__(pack_tensor, unpack_tensor) def update_model_params(self, model: nn.Module): """ Update the set of parameter storage pointers from the model. This allows filtering out model parameters during offloading, which is especially important for FSDP models where parameters may not be detected by isinstance checks. For FSDP v2, this method handles DTensor parameters which may be sharded across ranks and not have valid local storage on all ranks. We extract the local tensor from DTensors using _local_tensor when available. Args: model: The model whose parameters should be tracked """ param_storages = set() for p in model.parameters(): # For FSDP v2: extract local tensor from DTensor actual_tensor = p if DTensor is not None and isinstance(p, DTensor) and hasattr(p, "_local_tensor"): actual_tensor = p._local_tensor # Try to get storage pointer try: storage_ptr = actual_tensor.untyped_storage().data_ptr() if storage_ptr != 0: param_storages.add(storage_ptr) except RuntimeError: # Parameter doesn't have accessible storage (e.g., FSDP v2 sharded without local shard, FP8 parameters) # These will be caught by other checks (isinstance for Parameter, class name for FP8) continue self.param_storages = param_storages class NoOpManager(saved_tensors_hooks): """ A `saved_tensors_hook` manager used to disable any other `saved_tensors_hook` manager applied before. This relies on the behavior that only the most recently registered `saved_tensors_hook` will run. One example usage is to opt a local region of code out of activations offloading, which is usually applied globally to best track state. """ def __init__(self) -> None: def noop(tensor): return tensor super().__init__(noop, noop) def get_act_offloading_ctx_manager( model: nn.Module, use_pin_memory: bool = True, use_streams: bool = True, min_offload_size: int = 1024, max_fwd_stash_size: int = 5, warn_if_no_head: bool = True, ) -> OffloadActivations: """ Returns the activation offloading context manager for the model. All but the last output Linear in every step will be offloaded. If activation offloading is enabled, we return the OffloadActivations context manager. If activation offloading is disabled, we return a NoOpManager context manager. Args: model (`nn.Module`): Model to wrap with the activation offloading context manager. use_pin_memory (`bool`, *optional*, defaults to `True`): Whether to offloaded Tensor will be placed in pinned memory on the CPU. Pinned memory allows the Tensor to be moved back onto GPU more quickly but is a limited resource. use_streams (`bool`, *optional*, defaults to `True`): Whether to use streams for performance optimization where the communications get overlapped with the computation. Requires a torch build after torch-2.5.0. min_offload_size (`int`, *optional*, defaults to `1024`): Minimum number of bytes a Tensor must be in order to qualify for offloading. If the tensor is too small, we do not want to waste bandwidth and resources moving it to CPU and back. max_fwd_stash_size (`int`, *optional*, defaults to `5`): Maximum size of the forward stash, or the maximum number of consecutive activations to keep alive during the forward pass. This number must be at least 1. Keeping alive more activations will potentially allow more overlap between the communication and compute streams at the cost of increasing memory usage. Keeping alive fewer activations will conserve memory, but may cause poor overlap between the streams, increasing runtime. warn_if_no_head (`bool`, *optional*, defaults to `True`): Whether to warn if no output head is detected. If set to `False`, no warning will be raised if no output head is detected. Returns: `contextlib.ContextDecorator`: Activation offloading context manager for the model. """ activations_handling_ctx = OffloadActivations( use_pin_memory=use_pin_memory, use_streams=use_streams, min_offload_size=min_offload_size, max_fwd_stash_size=max_fwd_stash_size, ) # Update parameter storages to filter them during offloading (important for FSDP) activations_handling_ctx.update_model_params(model) # Below is our hack to disable offloading the last output Linear in every # step, as the cost for offloading the activation and then soon after bringing # it back is expensive. output_head_detected = False noop_ctx = NoOpManager() # Try to get the actual model if it's wrapped unwrapped_model = model if hasattr(unwrapped_model, "module"): unwrapped_model = unwrapped_model.module # check for PEFT models if hasattr(unwrapped_model, "base_model") and hasattr(unwrapped_model, "peft_config"): unwrapped_model = unwrapped_model.base_model # Check for different types of output heads if hasattr(unwrapped_model, "output"): if isinstance(unwrapped_model.output, nn.Module): unwrapped_model.output.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) unwrapped_model.output.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True elif hasattr(unwrapped_model.output, "linear") and isinstance(unwrapped_model.output.linear, nn.Module): unwrapped_model.output.linear.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) unwrapped_model.output.linear.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True # Check for HuggingFace model output heads elif hasattr(unwrapped_model, "lm_head"): unwrapped_model.lm_head.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) unwrapped_model.lm_head.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True # Check for decoder-based models elif hasattr(unwrapped_model, "decoder"): decoder = unwrapped_model.decoder if hasattr(decoder, "output"): decoder.output.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) decoder.output.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True # Some models have lm_head in the decoder elif hasattr(decoder, "lm_head"): decoder.lm_head.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) decoder.lm_head.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True # Check for transformer models with final layer norm elif hasattr(unwrapped_model, "final_layer_norm") or hasattr(unwrapped_model, "ln_f"): final_norm = getattr(unwrapped_model, "final_layer_norm", None) or unwrapped_model.ln_f final_norm.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) final_norm.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True # Check for models with head module elif hasattr(unwrapped_model, "head") and isinstance(unwrapped_model.head, nn.Module): unwrapped_model.head.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) unwrapped_model.head.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) output_head_detected = True if not output_head_detected and warn_if_no_head: logger.warning( "During activation offloading, no output head was detected. If your model has an output head, it will be " "offloaded. This usually greatly slows training, given the large vocabulary size. To change this " "behavior, set your output head as model.output and make it an nn.Module. You can disable this warning by " "passing `warn_if_no_head=False`." ) # Disable offloading for any Liger modules for name, module in unwrapped_model.named_modules(): if "liger" in name.lower(): module.register_forward_pre_hook(lambda *args: noop_ctx.__enter__()) module.register_forward_hook(lambda *args: noop_ctx.__exit__(), always_call=True) return activations_handling_ctx ================================================ FILE: trl/models/utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import itertools import warnings from collections.abc import Callable from contextlib import contextmanager from copy import deepcopy from typing import TYPE_CHECKING, Any import accelerate import torch.nn as nn import transformers from accelerate import Accelerator from packaging.version import Version from torch.distributed.fsdp import FSDPModule from torch.distributed.fsdp.fully_sharded_data_parallel import FullyShardedDataParallel as FSDP from transformers import GenerationConfig, PreTrainedModel from ..import_utils import suppress_experimental_warning with suppress_experimental_warning(): from ..experimental.utils import create_reference_model as _create_reference_model if Version(accelerate.__version__) >= Version("1.11.0"): from accelerate.utils.fsdp_utils import get_parameters_from_modules if TYPE_CHECKING: from deepspeed.runtime.engine import DeepSpeedEngine from torch.nn import Module from torch.nn.parallel.distributed import DistributedDataParallel def remove_hooks(model: "DeepSpeedEngine") -> None: """Removes the optimizer hooks from a DeepSpeed ZeRO-3 model.""" if not hasattr(model, "optimizer"): # before the first training step, the model has no optimizer return if model.optimizer is not None and hasattr(model.optimizer, "parameter_offload"): optimizer_offload = model.optimizer.parameter_offload elif model.optimizer is not None: optimizer_offload = model.optimizer else: raise RuntimeError("The model optimizer is None, which is not yet supported.") for param in iter_params(optimizer_offload.module, recurse=True): param.ds_active_sub_modules.clear() for hook in optimizer_offload.forward_hooks: hook.remove() for hook in optimizer_offload.backward_hooks: hook.remove() optimizer_offload.forward_hooks = [] optimizer_offload.backward_hooks = [] def get_all_parameters(sub_module, recurse=False): return itertools.chain(sub_module.named_parameters(recurse=recurse), sub_module.ds_external_parameters()) def iter_params(module, recurse=False): return [param for _, param in get_all_parameters(module, recurse)] def add_hooks(model: "DeepSpeedEngine") -> None: """Adds the optimizer hooks from a DeepSpeed ZeRO-3 model.""" import deepspeed if not hasattr(model, "optimizer"): # before the first training step, the model has no optimizer return if model.optimizer is not None and hasattr(model.optimizer, "parameter_offload"): optimizer_offload = model.optimizer.parameter_offload elif model.optimizer is not None: optimizer_offload = model.optimizer else: raise RuntimeError("The model optimizer is None, which is not yet supported.") if Version(deepspeed.__version__) >= Version("0.16.4"): # Account for renaming in https://github.com/deepspeedai/DeepSpeed/pull/6847 optimizer_offload._register_deepspeed_module(optimizer_offload.module) else: optimizer_offload._register_hooks_recursively(optimizer_offload.module) @contextmanager def _unwrap_model_for_generation( model: "DistributedDataParallel | DeepSpeedEngine", accelerator: "Accelerator", gather_deepspeed3_params: bool = True, ): """ Context manager to unwrap distributed or accelerated models for generation tasks. Args: model (`DistributedDataParallel | DeepSpeedEngine`): Model to be unwrapped. accelerator ([`~accelerate.Accelerator`]): Accelerator instance managing the model. gather_deepspeed3_params (`bool`, *optional*, defaults to `True`): Whether to gather weights for DeepSpeed ZeRO Stage 3 models. If `False`, skips parameter gathering, which can be more memory-efficient but may lead to slower generation times. Yields: Unwrapped model. Example: ```python with _unwrap_model_for_generation(model, accelerator) as unwrapped_model: generated_outputs = unwrapped_model.generate(input_ids) ``` """ unwrapped_model = accelerator.unwrap_model(model) is_gradient_checkpointing = unwrapped_model.is_gradient_checkpointing if is_gradient_checkpointing: unwrapped_model.gradient_checkpointing_disable() if accelerator.state.deepspeed_plugin is not None and accelerator.state.deepspeed_plugin.zero_stage == 3: if not gather_deepspeed3_params: yield accelerator.unwrap_model(model) else: import deepspeed with deepspeed.zero.GatheredParameters(model.parameters()): remove_hooks(model) yield accelerator.unwrap_model(model) add_hooks(model) else: yield unwrapped_model if is_gradient_checkpointing: unwrapped_model.gradient_checkpointing_enable() @contextmanager def _override_model_generation_config(model, generation_kwargs=None): """ Context manager to temporarily override a model's generation_config with training config. This works around transformers' config merging logic that would otherwise overwrite values matching global defaults with model-specific values (see upstream issue transformers#42762; fixed in transformers v5 by PR `transformers#42702`). By temporarily setting the model's generation_config to match the passed generation_config, we avoid the conflict. The model's original generation_config is preserved outside this context, ensuring that saved/pushed models retain their intended inference behavior. Args: model: The model (typically unwrapped_model) whose generation_config to temporarily override. generation_kwargs (dict): Generation kwargs to be used to override model's generation config. """ if ( # Issue fixed in transformers v5 by PR transformers#42702 Version(transformers.__version__) >= Version("5.0.0") or generation_kwargs is None or not hasattr(model, "generation_config") ): yield model return # If it is a PEFT model, override the underlying base model if hasattr(model, "get_base_model"): model = model.get_base_model() # Keep original model generation_config original_config = model.generation_config # Create training-specific generation config from the model's original generation config # Then overwrite it with the training-specific generation kwargs generation_config = GenerationConfig.from_dict(model.generation_config.to_dict()) generation_config.update(**generation_kwargs) model.generation_config = generation_config try: yield finally: model.generation_config = original_config @contextmanager def unwrap_model_for_generation( model: "DistributedDataParallel | DeepSpeedEngine", accelerator: "Accelerator", gather_deepspeed3_params: bool = True, generation_kwargs: dict | None = None, ): """ Context manager to unwrap distributed or accelerated models for generation tasks. This function unwraps distributed models (FSDP, DeepSpeed) and optionally overrides the model's generation_config temporarily during generation. This is useful for applying training-specific generation parameters without permanently modifying the model's original generation_config. Args: model (`DistributedDataParallel | DeepSpeedEngine`): Model to be unwrapped. accelerator ([`~accelerate.Accelerator`]): Accelerator instance managing the model. gather_deepspeed3_params (`bool`, *optional*, defaults to `True`): Whether to gather weights for DeepSpeed ZeRO Stage 3 models. If `False`, skips parameter gathering, which can be more memory-efficient but may lead to slower generation times. generation_kwargs (dict, *optional*): If provided, temporarily overrides the model's generation_config during generation. The original config is automatically restored when exiting the context. This is useful for using different generation parameters during training vs. inference. Yields: Unwrapped model with optionally overridden generation_config. """ with ( _unwrap_model_for_generation( model, accelerator, gather_deepspeed3_params=gather_deepspeed3_params ) as unwrapped_model, _override_model_generation_config(unwrapped_model, generation_kwargs=generation_kwargs), ): yield unwrapped_model def prepare_deepspeed(model: "Module", accelerator: "Accelerator"): """Prepares the model for DeepSpeed inference or evaluation by initializing it with the appropriate configuration. Adapted from accelerate: https://github.com/huggingface/accelerate/blob/739b135f8367becb67ffaada12fe76e3aa60fefd/src/accelerate/accelerator.py#L1473 """ import deepspeed # local import (instead of top-level) to avoid DS init interfering with other backends (like vllm): https://github.com/deepspeedai/DeepSpeed/issues/7252 deepspeed_plugin = accelerator.state.deepspeed_plugin config_kwargs = deepcopy(deepspeed_plugin.deepspeed_config) stage = config_kwargs["zero_optimization"]["stage"] if model is not None: hidden_size = ( max(model.config.hidden_sizes) if getattr(model.config, "hidden_sizes", None) else getattr(model.config, "hidden_size", None) ) if hidden_size is not None and stage == 3: # Note that `stage3_prefetch_bucket_size` can produce DeepSpeed messages like: `Invalidate trace cache # @ step 0: expected module 1, but got module 0` # This is expected and is not an error, see: https://github.com/microsoft/DeepSpeed/discussions/4081 config_kwargs.update( { "zero_optimization.reduce_bucket_size": hidden_size * hidden_size, "zero_optimization.stage3_param_persistence_threshold": 10 * hidden_size, "zero_optimization.stage3_prefetch_bucket_size": 0.9 * hidden_size * hidden_size, } ) # If ZeRO-3 is used, we shard both the active and reference model. # Otherwise, we assume the reference model fits in memory and is initialized on each device with ZeRO # disabled (stage 0) if stage != 3: config_kwargs["zero_optimization"]["stage"] = 0 model, *_ = deepspeed.initialize(model=model, config=config_kwargs) model.eval() return model def prepare_fsdp(model, accelerator: Accelerator) -> FSDP | FSDPModule: # Check if the model is already a FSDP model due to `Manual Wrapping` and if so, don't wrap it again if not isinstance(model, (FSDP, FSDPModule)): fsdp_plugin = accelerator.state.fsdp_plugin if fsdp_plugin.fsdp_version == 1: accelerator.state.fsdp_plugin.set_auto_wrap_policy(model) kwargs = { "sharding_strategy": fsdp_plugin.sharding_strategy or fsdp_plugin.reshard_after_forward, "cpu_offload": fsdp_plugin.cpu_offload, "auto_wrap_policy": fsdp_plugin.auto_wrap_policy, "mixed_precision": fsdp_plugin.mixed_precision_policy, "sync_module_states": fsdp_plugin.sync_module_states, "backward_prefetch": fsdp_plugin.backward_prefetch, "forward_prefetch": fsdp_plugin.forward_prefetch, "use_orig_params": fsdp_plugin.use_orig_params, "param_init_fn": fsdp_plugin.param_init_fn, "ignored_modules": fsdp_plugin.ignored_modules, "limit_all_gathers": fsdp_plugin.limit_all_gathers, "device_id": accelerator.device, } model = FSDP(model, **kwargs) elif fsdp_plugin.fsdp_version == 2: from torch.distributed.fsdp import MixedPrecisionPolicy, fully_shard mesh = getattr(accelerator, "torch_device_mesh", None) if Version(accelerate.__version__) >= Version("1.11.0"): ignored_params = get_parameters_from_modules(fsdp_plugin.ignored_modules, model, accelerator.device) else: warnings.warn( "FSDP version 2 is being used with accelerate version < 1.11.0, which may lead to incorrect " "handling of ignored modules. Please upgrade accelerate to v1.11.0 or later for proper support." ) ignored_params = None fully_shard( model, reshard_after_forward=fsdp_plugin.reshard_after_forward, offload_policy=fsdp_plugin.cpu_offload, # `fully_shard` doesn't accept `None` in case of `MixedPrecisionPolicy` mp_policy=fsdp_plugin.mixed_precision_policy or MixedPrecisionPolicy(), mesh=mesh[tuple(accelerator.parallelism_config.fsdp_dim_names)] if mesh is not None else None, ignored_params=ignored_params, ) else: raise ValueError(f"FSDP version {fsdp_plugin.fsdp_version} is not supported.") model.eval() return model class _ForwardRedirection: """Implements the `forward-redirection`. Taken from Pytorch-lightning: https://github.com/Lightning-AI/pytorch-lightning/blob/02311d03fb982560246eead7c08104481fac9579/src/lightning/pytorch/strategies/strategy.py#L602 A method call to a wrapped module gets rerouted through the wrapper's `forward` method instead. """ def __call__( self, wrapper_module: nn.Module, original_module: nn.Module, method: Callable, *args: Any, **kwargs: Any ): """Reroutes a method call through the `wrapper_module`'s `forward` method. Args: wrapper_module: The module that has `original_module` wrapped. original_module: The module that was wrapped inside `wrapper_module`. method: The method that should be called on the `original_module` after inputs get redirected through the `wrapper_module`'s `forward` method. *args: The positional arguments to the `method`. They will get passed to a patched `forward` method instead. **kwargs: The keyword arguments to the `method`. They will get passed to a patched `forward` method instead. """ original_forward = original_module.forward def wrapped_forward(*_args: Any, **_kwargs: Any) -> Any: # Unpatch ourselves immediately before calling the method `method_name` # because itself may want to call the real `forward` original_module.forward = original_forward # type: ignore[method-assign] # Call the actual method e.g. `.training_step(...)` out = method(*_args, **_kwargs) self.on_after_inner_forward(wrapper_module, original_module) return out # Patch the original_module's forward so we can redirect the arguments back to the real method original_module.forward = wrapped_forward # type: ignore[method-assign] wrapper_output = wrapper_module(*args, **kwargs) self.on_after_outer_forward(wrapper_module, original_module) return wrapper_output def on_after_inner_forward(self, wrapper_module: nn.Module, original_module: nn.Module) -> None: pass def on_after_outer_forward(self, wrapper_module: nn.Module, original_module: nn.Module) -> None: pass @contextmanager def disable_gradient_checkpointing(model: PreTrainedModel, gradient_checkpointing_kwargs: dict | None = None): """ Temporarily disable gradient checkpointing, restoring the previous state afterward. Args: model (`PreTrainedModel`): Model for which to temporarily disable gradient checkpointing. gradient_checkpointing_kwargs (`dict` or `None`, *optional*): Additional kwargs for gradient checkpointing enabling. """ was_enabled = model.is_gradient_checkpointing if was_enabled: model.gradient_checkpointing_disable() try: yield finally: if was_enabled: model.gradient_checkpointing_enable(gradient_checkpointing_kwargs) def create_reference_model( model: nn.Module, num_shared_layers: int | None = None, pattern: str | None = None ) -> nn.Module: warnings.warn( "The `create_reference_model` function is now located in `trl.experimental.utils`. Please update your " "imports to `from trl.experimental.utils import create_reference_model`. This import path will be removed in " "TRL 1.0.0.", FutureWarning, stacklevel=2, ) return _create_reference_model(model, num_shared_layers=num_shared_layers, pattern=pattern) ================================================ FILE: trl/py.typed ================================================ ================================================ FILE: trl/rewards/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from typing import TYPE_CHECKING from .._lazy_module import _LazyModule _import_structure = { "accuracy_rewards": ["accuracy_reward", "reasoning_accuracy_reward"], "format_rewards": ["think_format_reward"], "other_rewards": ["get_soft_overlong_punishment"], } if TYPE_CHECKING: from .accuracy_rewards import accuracy_reward, reasoning_accuracy_reward from .format_rewards import think_format_reward from .other_rewards import get_soft_overlong_punishment else: sys.modules[__name__] = _LazyModule(__name__, __file__, _import_structure, module_spec=__spec__) ================================================ FILE: trl/rewards/accuracy_rewards.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import threading from ..import_utils import is_math_verify_available if is_math_verify_available(): from latex2sympy2_extended import NormalizationConfig from math_verify import LatexExtractionConfig, parse, verify def accuracy_reward(completions: list[list[dict[str, str]]], solution: list[str], **kwargs) -> list[float | None]: r""" Reward function that checks if the completion matches the ground truth. - If both gold and prediction are parseable → use math verification. - If gold is not parseable → return `None` to skip the example. Args: completions (`list[list[dict[str, str]]]`): List of completions to be evaluated. Each completion must be a list of one message, i.e. a dictionary containing the key `"content"` with the value being the text of the completion. solution: (`list[str]`): List of the raw-text solutions to the questions/problems/prompts. **kwargs: Additional keyword arguments. This function does not use them, but they are required in the function signature to ensure compatibility with trainers like [`GRPOTrainer`]. Example: ```python >>> from trl.rewards import accuracy_reward >>> solutions = [r"\frac{1}{3}", r"\frac{1}{3}"] >>> completions = [ ... [{"role": "assistant", "content": r"My answer is \boxed{\frac{1}{3}}"}], ... [{"role": "assistant", "content": r"My answer is \boxed{\frac{1}{2}}"}], ... ] >>> accuracy_reward(completions, solutions) [1.0, 0.0] ``` """ if not is_math_verify_available(): raise ImportError("Please install the `math_verify` package to use accuracy_reward") contents = [completion[0]["content"] for completion in completions] rewards = [] # math_verify uses signal.alarm() for timeouts, which only works in the main thread. # Disable timeouts when running in a non-main thread to avoid ValueError. is_main_thread = threading.current_thread() is threading.main_thread() parsing_timeout = None if not is_main_thread else 10 verify_timeout = None if not is_main_thread else 5 # Suppress the "Timeout is disabled" warnings from math_verify when we intentionally disable timeouts if not is_main_thread: logging.getLogger("math_verify.parser").setLevel(logging.ERROR) logging.getLogger("math_verify.grader").setLevel(logging.ERROR) for content, sol in zip(contents, solution, strict=True): gold_parsed = parse(sol, parsing_timeout=parsing_timeout) if len(gold_parsed) != 0: # We require the answer to be provided in correct latex (no malformed operators) answer_parsed = parse( content, extraction_config=[ LatexExtractionConfig( normalization_config=NormalizationConfig(units=True), # Ensures that boxed is tried first boxed_match_priority=0, try_extract_without_anchor=False, ) ], extraction_mode="first_match", parsing_timeout=parsing_timeout, ) reward = float(verify(gold_parsed, answer_parsed, timeout_seconds=verify_timeout)) else: # If the gold solution cannot be parsed, we assign `None` to skip this example reward = None rewards.append(reward) return rewards def reasoning_accuracy_reward( completions: list[list[dict[str, str]]], solution: list[str], reasoning_delimiters: list[str] | None = None, **kwargs, ) -> list[float | None]: r""" Reward function that removes the reasoning content and checks if the final answer matches the ground truth. - If both gold and prediction are parseable → use math verification. - If gold is not parseable → return `None` to skip the example. Args: completions (`list[list[dict[str, str]]]`): List of completions to be evaluated. Each completion must be a list of one message, i.e. a dictionary containing the key `"content"` with the value being the text of the completion. solution: (`list[str]`): List of the raw-text solutions to the questions/problems/prompts. reasoning_delimiters (`list[str]]`, *optional*): List of strings indicating where the reasoning content ends. The final answer is assumed to be after the last occurrence of any of these delimiters. If `None`, defaults to `[""]`. **kwargs: Additional keyword arguments. This function does not use them, but they are required in the function signature to ensure compatibility with trainers like [`GRPOTrainer`]. Example: ```python >>> from trl.rewards import reasoning_accuracy_reward >>> reasoning_delimiters = [""] >>> solutions = [r"\frac{1}{3}", r"\frac{1}{3}", r"\frac{1}{3}"] >>> completions = [ ... [ ... { ... "role": "assistant", ... "content": r" Reasoning content The final answer is \boxed{\frac{1}{3}}", ... } ... ], ... [ ... { ... "role": "assistant", ... "content": r" Reasoning content The final answer is \boxed{\frac{1}{2}}", ... } ... ], ... [ ... { ... "role": "assistant", ... "content": r" Reasoning content with partial answers \boxed{\frac{1}{3}} but no final answer", ... } ... ], ... ] >>> reasoning_accuracy_reward(completions, solutions, reasoning_delimiters=reasoning_delimiters) [1.0, 0.0, 0.0] ``` """ if not is_math_verify_available(): raise ImportError("Please install the `math_verify` package to use reasoning_accuracy_reward") if reasoning_delimiters is None: # Use sensible defaults for majority of reasoning models reasoning_delimiters = [""] rewards = [] contents = [completion[0]["content"] for completion in completions] # math_verify uses signal.alarm() for timeouts, which only works in the main thread. # Disable timeouts when running in a non-main thread to avoid ValueError. is_main_thread = threading.current_thread() is threading.main_thread() parsing_timeout = None if not is_main_thread else 10 verify_timeout = None if not is_main_thread else 5 # Suppress the "Timeout is disabled" warnings from math_verify when we intentionally disable timeouts if not is_main_thread: logging.getLogger("math_verify.parser").setLevel(logging.ERROR) logging.getLogger("math_verify.grader").setLevel(logging.ERROR) for content, sol in zip(contents, solution, strict=True): # Split final answer from reasoning content is_reasoning_complete = False for delim in reasoning_delimiters: if delim in content: content = content.split(delim)[-1] is_reasoning_complete = True break if not is_reasoning_complete: # We assign zero reward instead of `None` to penalize incomplete reasoning rewards.append(0.0) continue gold_parsed = parse(sol, parsing_timeout=parsing_timeout) if len(gold_parsed) != 0: # We require the answer to be provided in correct latex (no malformed operators) answer_parsed = parse( content, extraction_config=[ LatexExtractionConfig( boxed_match_priority=0, normalization_config=NormalizationConfig( units=True, ), try_extract_without_anchor=False, ) ], extraction_mode="first_match", parsing_timeout=parsing_timeout, ) reward = float(verify(gold_parsed, answer_parsed, timeout_seconds=verify_timeout)) else: # If the gold solution cannot be parsed, we assign `None` to skip this example reward = None rewards.append(reward) return rewards ================================================ FILE: trl/rewards/format_rewards.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re def think_format_reward(completions: list[list[dict[str, str]]], **kwargs) -> list[float]: r""" Reward function that checks if the reasoning process is enclosed within `""` and `""` tags. The function returns a reward of 1.0 if the format is correct, otherwise 0.0. Args: completions (`list[list[dict[str, str]]]`): List of completions to be evaluated. Each completion must be a list of one message, i.e. a dictionary containing the key `"content"` with the value being the text of the completion. **kwargs: Additional keyword arguments. This function does not use them, but they are required in the function signature to ensure compatibility with trainers like [`GRPOTrainer`]. Returns: `list[float]`: A list of rewards, where each reward is 1.0 if the completion matches the expected format, otherwise 0.0. Example: ```python >>> from trl.rewards import think_format_reward >>> completions = [ ... [{"content": "\nThis is my reasoning.\n\nThis is my answer."}], ... [{"content": "\nThis is my reasoning.\nThis is my answer."}], ... ] >>> think_format_reward(completions) [1.0, 0.0] ``` """ pattern = r"^(?!.*)(.*?).*$" completion_contents = [completion[0]["content"] for completion in completions] matches = [re.match(pattern, content, re.DOTALL | re.MULTILINE) for content in completion_contents] return [1.0 if match else 0.0 for match in matches] ================================================ FILE: trl/rewards/other_rewards.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from collections.abc import Callable def get_soft_overlong_punishment(max_completion_len: int, soft_punish_cache: int) -> Callable: # docstyle-ignore r""" Reward function that penalizes overlong completions. It is used to penalize overlong completions, but not to reward shorter completions. Reference: Eq. (13) from the DAPO paper (https://huggingface.co/papers/2503.14476) $$ R_{\text{length}}(y) = \begin{cases} 0, & |y| \le L_{\max} - L_{\text{cache}} \\ \dfrac{(L_{\max} - L_{\text{cache}}) - |y|}{L_{\text{cache}}}, & L_{\max} - L_{\text{cache}} < |y| \le L_{\max} \\ -1, & L_{\max} < |y| \end{cases} $$ Args: max_completion_len (`int`): Maximum length of the completion, \( L_{\max} \). soft_punish_cache (`int`): Minimum length of the completion, \( L_{\text{cache}} \). If set to `0`, no minimum length is applied. Example: ```python from trl.rewards import get_soft_overlong_punishment soft_overlong_punishment = get_soft_overlong_punishment(max_completion_len=100, soft_punish_cache=20) completion_ids = [[1] * 90] # simulating a completion with 90 tokens. 90 is between 80 and 100. rewards = soft_overlong_punishment(completion_ids) print(rewards) # [-0.5] ``` """ def soft_overlong_punishment_reward(completion_ids: list[list[int]], **kwargs) -> list[float]: """Reward function that penalizes overlong completions.""" rewards = [] for ids in completion_ids: completion_length = len(ids) if completion_length <= max_completion_len - soft_punish_cache: rewards.append(0.0) elif max_completion_len - soft_punish_cache < completion_length <= max_completion_len: rewards.append((max_completion_len - soft_punish_cache - completion_length) / soft_punish_cache) else: rewards.append(-1.0) return rewards return soft_overlong_punishment_reward ================================================ FILE: trl/scripts/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import TYPE_CHECKING from .._lazy_module import _LazyModule _import_structure = { "utils": ["DatasetMixtureConfig", "ScriptArguments", "TrlParser", "get_dataset", "init_zero_verbose"], } if TYPE_CHECKING: from .utils import DatasetMixtureConfig, ScriptArguments, TrlParser, get_dataset, init_zero_verbose else: import sys sys.modules[__name__] = _LazyModule(__name__, globals()["__file__"], _import_structure, module_spec=__spec__) ================================================ FILE: trl/scripts/_hf_argparser.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Copied from: https://github.com/huggingface/transformers/blob/3a275d3581c0ecf962f7412aa764c2047331fd6b/src/transformers/hf_argparser.py # This avoids an upstream latency issue: https://github.com/huggingface/transformers/issues/44273 # - Moved yaml import inside function import dataclasses import json import os import sys import types from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError from collections.abc import Callable, Iterable from copy import copy from enum import Enum from inspect import isclass from pathlib import Path from typing import Any, Literal, NewType, Union, get_type_hints DataClass = NewType("DataClass", Any) DataClassType = NewType("DataClassType", Any) # From https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse def string_to_bool(v): if isinstance(v, bool): return v if v.lower() in ("yes", "true", "t", "y", "1"): return True elif v.lower() in ("no", "false", "f", "n", "0"): return False else: raise ArgumentTypeError( f"Truthy value expected: got {v} but expected one of yes/no, true/false, t/f, y/n, 1/0 (case insensitive)." ) def make_choice_type_function(choices: list) -> Callable[[str], Any]: """ Creates a mapping function from each choices string representation to the actual value. Used to support multiple value types for a single argument. Args: choices (list): List of choices. Returns: Callable[[str], Any]: Mapping function from string representation to actual value for each choice. """ str_to_choice = {str(choice): choice for choice in choices} return lambda arg: str_to_choice.get(arg, arg) def HfArg( *, aliases: str | list[str] | None = None, help: str | None = None, default: Any = dataclasses.MISSING, default_factory: Callable[[], Any] = dataclasses.MISSING, metadata: dict | None = None, **kwargs, ) -> dataclasses.Field: """Argument helper enabling a concise syntax to create dataclass fields for parsing with `HfArgumentParser`. Example comparing the use of `HfArg` and `dataclasses.field`: ``` @dataclass class Args: regular_arg: str = dataclasses.field(default="Huggingface", metadata={"aliases": ["--example", "-e"], "help": "This syntax could be better!"}) hf_arg: str = HfArg(default="Huggingface", aliases=["--example", "-e"], help="What a nice syntax!") ``` Args: aliases (Union[str, list[str]], optional): Single string or list of strings of aliases to pass on to argparse, e.g. `aliases=["--example", "-e"]`. Defaults to None. help (str, optional): Help string to pass on to argparse that can be displayed with --help. Defaults to None. default (Any, optional): Default value for the argument. If not default or default_factory is specified, the argument is required. Defaults to dataclasses.MISSING. default_factory (Callable[[], Any], optional): The default_factory is a 0-argument function called to initialize a field's value. It is useful to provide default values for mutable types, e.g. lists: `default_factory=list`. Mutually exclusive with `default=`. Defaults to dataclasses.MISSING. metadata (dict, optional): Further metadata to pass on to `dataclasses.field`. Defaults to None. Returns: Field: A `dataclasses.Field` with the desired properties. """ if metadata is None: # Important, don't use as default param in function signature because dict is mutable and shared across function calls metadata = {} if aliases is not None: metadata["aliases"] = aliases if help is not None: metadata["help"] = help return dataclasses.field(metadata=metadata, default=default, default_factory=default_factory, **kwargs) class HfArgumentParser(ArgumentParser): """ This subclass of `argparse.ArgumentParser` uses type hints on dataclasses to generate arguments. The class is designed to play well with the native argparse. In particular, you can add more (non-dataclass backed) arguments to the parser after initialization and you'll get the output back after parsing as an additional namespace. Optional: To create sub argument groups use the `_argument_group_name` attribute in the dataclass. Args: dataclass_types (`DataClassType` or `Iterable[DataClassType]`, *optional*): Dataclass type, or list of dataclass types for which we will "fill" instances with the parsed args. kwargs (`dict[str, Any]`, *optional*): Passed to `argparse.ArgumentParser()` in the regular way. """ dataclass_types: Iterable[DataClassType] def __init__(self, dataclass_types: DataClassType | Iterable[DataClassType] | None = None, **kwargs): # Make sure dataclass_types is an iterable if dataclass_types is None: dataclass_types = [] elif not isinstance(dataclass_types, Iterable): dataclass_types = [dataclass_types] # To make the default appear when using --help if "formatter_class" not in kwargs: kwargs["formatter_class"] = ArgumentDefaultsHelpFormatter super().__init__(**kwargs) if dataclasses.is_dataclass(dataclass_types): dataclass_types = [dataclass_types] self.dataclass_types = list(dataclass_types) for dtype in self.dataclass_types: self._add_dataclass_arguments(dtype) @staticmethod def _parse_dataclass_field(parser: ArgumentParser, field: dataclasses.Field): # Long-option strings are conventionlly separated by hyphens rather # than underscores, e.g., "--long-format" rather than "--long_format". # Argparse converts hyphens to underscores so that the destination # string is a valid attribute name. Hf_argparser should do the same. long_options = [f"--{field.name}"] if "_" in field.name: long_options.append(f"--{field.name.replace('_', '-')}") kwargs = field.metadata.copy() # field.metadata is not used at all by Data Classes, # it is provided as a third-party extension mechanism. if isinstance(field.type, str): raise RuntimeError( "Unresolved type detected, which should have been done with the help of " "`typing.get_type_hints` method by default" ) aliases = kwargs.pop("aliases", []) if isinstance(aliases, str): aliases = [aliases] origin_type = getattr(field.type, "__origin__", field.type) if origin_type is Union or (hasattr(types, "UnionType") and isinstance(origin_type, types.UnionType)): if str not in field.type.__args__ and ( len(field.type.__args__) != 2 or type(None) not in field.type.__args__ ): raise ValueError( "Only `Union[X, NoneType]` (i.e., `Optional[X]`) is allowed for `Union` because" " the argument parser only supports one type per argument." f" Problem encountered in field '{field.name}'." ) if type(None) not in field.type.__args__: # filter `str` in Union field.type = field.type.__args__[0] if field.type.__args__[1] is str else field.type.__args__[1] origin_type = getattr(field.type, "__origin__", field.type) elif bool not in field.type.__args__: # filter `NoneType` in Union (except for `Union[bool, NoneType]`) field.type = ( field.type.__args__[0] if isinstance(None, field.type.__args__[1]) else field.type.__args__[1] ) origin_type = getattr(field.type, "__origin__", field.type) # A variable to store kwargs for a boolean field, if needed # so that we can init a `no_*` complement argument (see below) bool_kwargs = {} if origin_type is Literal or (isinstance(field.type, type) and issubclass(field.type, Enum)): if origin_type is Literal: kwargs["choices"] = field.type.__args__ else: kwargs["choices"] = [x.value for x in field.type] kwargs["type"] = make_choice_type_function(kwargs["choices"]) if field.default is not dataclasses.MISSING: kwargs["default"] = field.default else: kwargs["required"] = True elif field.type is bool or field.type == bool | None: # Copy the correct kwargs to use to instantiate a `no_*` complement argument below. # We do not initialize it here because the `no_*` alternative must be instantiated after the real argument bool_kwargs = copy(kwargs) # Hack because type=bool in argparse does not behave as we want. kwargs["type"] = string_to_bool if field.type is bool or (field.default is not None and field.default is not dataclasses.MISSING): # Default value is False if we have no default when of type bool. default = False if field.default is dataclasses.MISSING else field.default # This is the value that will get picked if we don't include --{field.name} in any way kwargs["default"] = default # This tells argparse we accept 0 or 1 value after --{field.name} kwargs["nargs"] = "?" # This is the value that will get picked if we do --{field.name} (without value) kwargs["const"] = True elif isclass(origin_type) and issubclass(origin_type, list): kwargs["type"] = field.type.__args__[0] kwargs["nargs"] = "+" if field.default_factory is not dataclasses.MISSING: kwargs["default"] = field.default_factory() elif field.default is dataclasses.MISSING: kwargs["required"] = True else: kwargs["type"] = field.type if field.default is not dataclasses.MISSING: kwargs["default"] = field.default elif field.default_factory is not dataclasses.MISSING: kwargs["default"] = field.default_factory() else: kwargs["required"] = True parser.add_argument(*long_options, *aliases, **kwargs) # Add a complement `no_*` argument for a boolean field AFTER the initial field has already been added. # Order is important for arguments with the same destination! # We use a copy of earlier kwargs because the original kwargs have changed a lot before reaching down # here and we do not need those changes/additional keys. if field.default is True and (field.type is bool or field.type == bool | None): bool_kwargs["default"] = False parser.add_argument( f"--no_{field.name}", f"--no-{field.name.replace('_', '-')}", action="store_false", dest=field.name, **bool_kwargs, ) def _add_dataclass_arguments(self, dtype: DataClassType): if hasattr(dtype, "_argument_group_name"): parser = self.add_argument_group(dtype._argument_group_name) else: parser = self try: type_hints: dict[str, type] = get_type_hints(dtype) except NameError: raise RuntimeError( f"Type resolution failed for {dtype}. Try declaring the class in global scope or " "removing line of `from __future__ import annotations` which opts in Postponed " "Evaluation of Annotations (PEP 563)" ) from None for field in dataclasses.fields(dtype): if not field.init: continue field.type = type_hints[field.name] self._parse_dataclass_field(parser, field) def parse_args_into_dataclasses( self, args=None, return_remaining_strings=False, look_for_args_file=True, args_filename=None, args_file_flag=None, ) -> tuple[DataClass, ...]: """ Parse command-line args into instances of the specified dataclass types. This relies on argparse's `ArgumentParser.parse_known_args`. See the doc at: docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_args Args: args: List of strings to parse. The default is taken from sys.argv. (same as argparse.ArgumentParser) return_remaining_strings: If true, also return a list of remaining argument strings. look_for_args_file: If true, will look for a ".args" file with the same base name as the entry point script for this process, and will append its potential content to the command line args. args_filename: If not None, will uses this file instead of the ".args" file specified in the previous argument. args_file_flag: If not None, will look for a file in the command-line args specified with this flag. The flag can be specified multiple times and precedence is determined by the order (last one wins). Returns: Tuple consisting of: - the dataclass instances in the same order as they were passed to the initializer.abspath - if applicable, an additional namespace for more (non-dataclass backed) arguments added to the parser after initialization. - The potential list of remaining argument strings. (same as argparse.ArgumentParser.parse_known_args) """ if args_file_flag or args_filename or (look_for_args_file and len(sys.argv)): args_files = [] if args_filename: args_files.append(Path(args_filename)) elif look_for_args_file and len(sys.argv): args_files.append(Path(sys.argv[0]).with_suffix(".args")) # args files specified via command line flag should overwrite default args files so we add them last if args_file_flag: # Create special parser just to extract the args_file_flag values args_file_parser = ArgumentParser() args_file_parser.add_argument(args_file_flag, type=str, action="append") # Use only remaining args for further parsing (remove the args_file_flag) cfg, args = args_file_parser.parse_known_args(args=args) cmd_args_file_paths = vars(cfg).get(args_file_flag.lstrip("-"), None) if cmd_args_file_paths: args_files.extend([Path(p) for p in cmd_args_file_paths]) file_args = [] for args_file in args_files: if args_file.exists(): file_args += args_file.read_text().split() # in case of duplicate arguments the last one has precedence # args specified via the command line should overwrite args from files, so we add them last args = file_args + args if args is not None else file_args + sys.argv[1:] namespace, remaining_args = self.parse_known_args(args=args) outputs = [] for dtype in self.dataclass_types: keys = {f.name for f in dataclasses.fields(dtype) if f.init} inputs = {k: v for k, v in vars(namespace).items() if k in keys} for k in keys: delattr(namespace, k) obj = dtype(**inputs) outputs.append(obj) if len(namespace.__dict__) > 0: # additional namespace. outputs.append(namespace) if return_remaining_strings: return (*outputs, remaining_args) else: if remaining_args: raise ValueError(f"Some specified arguments are not used by the HfArgumentParser: {remaining_args}") return (*outputs,) def parse_dict(self, args: dict[str, Any], allow_extra_keys: bool = False) -> tuple[DataClass, ...]: """ Alternative helper method that does not use `argparse` at all, instead uses a dict and populating the dataclass types. Args: args (`dict`): dict containing config values allow_extra_keys (`bool`, *optional*, defaults to `False`): Defaults to False. If False, will raise an exception if the dict contains keys that are not parsed. Returns: Tuple consisting of: - the dataclass instances in the same order as they were passed to the initializer. """ unused_keys = set(args.keys()) outputs = [] for dtype in self.dataclass_types: keys = {f.name for f in dataclasses.fields(dtype) if f.init} inputs = {k: v for k, v in args.items() if k in keys} unused_keys.difference_update(inputs.keys()) obj = dtype(**inputs) outputs.append(obj) if not allow_extra_keys and unused_keys: raise ValueError(f"Some keys are not used by the HfArgumentParser: {sorted(unused_keys)}") return tuple(outputs) def parse_json_file(self, json_file: str | os.PathLike, allow_extra_keys: bool = False) -> tuple[DataClass, ...]: """ Alternative helper method that does not use `argparse` at all, instead loading a json file and populating the dataclass types. Args: json_file (`str` or `os.PathLike`): File name of the json file to parse allow_extra_keys (`bool`, *optional*, defaults to `False`): Defaults to False. If False, will raise an exception if the json file contains keys that are not parsed. Returns: Tuple consisting of: - the dataclass instances in the same order as they were passed to the initializer. """ with open(Path(json_file), encoding="utf-8") as open_json_file: data = json.loads(open_json_file.read()) outputs = self.parse_dict(data, allow_extra_keys=allow_extra_keys) return tuple(outputs) def parse_yaml_file(self, yaml_file: str | os.PathLike, allow_extra_keys: bool = False) -> tuple[DataClass, ...]: """ Alternative helper method that does not use `argparse` at all, instead loading a yaml file and populating the dataclass types. Args: yaml_file (`str` or `os.PathLike`): File name of the yaml file to parse allow_extra_keys (`bool`, *optional*, defaults to `False`): Defaults to False. If False, will raise an exception if the json file contains keys that are not parsed. Returns: Tuple consisting of: - the dataclass instances in the same order as they were passed to the initializer. """ import yaml outputs = self.parse_dict(yaml.safe_load(Path(yaml_file).read_text()), allow_extra_keys=allow_extra_keys) return tuple(outputs) ================================================ FILE: trl/scripts/dpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ # Full training ```bash python trl/scripts/dpo.py \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --learning_rate 5.0e-7 \ --num_train_epochs 1 \ --per_device_train_batch_size 2 \ --max_steps 1000 \ --gradient_accumulation_steps 8 \ --eval_strategy steps \ --eval_steps 50 \ --output_dir Qwen2-0.5B-DPO \ --no_remove_unused_columns ``` # LoRA: ```bash python trl/scripts/dpo.py \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --learning_rate 5.0e-6 \ --num_train_epochs 1 \ --per_device_train_batch_size 2 \ --max_steps 1000 \ --gradient_accumulation_steps 8 \ --eval_strategy steps \ --eval_steps 50 \ --output_dir Qwen2-0.5B-DPO \ --no_remove_unused_columns \ --use_peft \ --lora_r 32 \ --lora_alpha 16 ``` """ import argparse import os # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args, dataset_args): import torch from accelerate import logging from datasets import load_dataset from transformers import AutoModelForCausalLM from trl import DPOTrainer, get_dataset, get_kbit_device_map, get_peft_config, get_quantization_config logger = logging.get_logger(__name__) ################ # Model ################### dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code, **model_kwargs ) peft_config = get_peft_config(model_args) if script_args.ignore_bias_buffers: # torch distributed hack model._ddp_params_and_buffers_to_ignore = [ name for name, buffer in model.named_buffers() if buffer.dtype == torch.bool ] # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the DPO trainer trainer = DPOTrainer( model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=peft_config, ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") if training_args.eval_strategy != "no": metrics = trainer.evaluate() trainer.log_metrics("eval", metrics) trainer.save_metrics("eval", metrics) # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, DPOConfig, ModelConfig, ScriptArguments, TrlParser dataclass_types = (ScriptArguments, DPOConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser("dpo", help="Run the DPO training script", dataclass_types=dataclass_types) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/env.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # ] # /// import os import platform from importlib.metadata import version def print_env(): import torch from accelerate.commands.config import default_config_file, load_config_from_file from transformers import is_bitsandbytes_available from transformers.utils import is_openai_available, is_peft_available from trl import __version__ from trl.import_utils import ( is_deepspeed_available, is_liger_kernel_available, is_llm_blender_available, is_vllm_available, ) from trl.scripts.utils import get_git_commit_hash devices = None if torch.cuda.is_available(): devices = [torch.cuda.get_device_name(i) for i in range(torch.cuda.device_count())] elif torch.backends.mps.is_available(): devices = ["MPS"] elif torch.xpu.is_available(): devices = [torch.xpu.get_device_name(i) for i in range(torch.xpu.device_count())] accelerate_config = accelerate_config_str = "not found" # Get the default from the config file. if os.path.isfile(default_config_file): accelerate_config = load_config_from_file(default_config_file).to_dict() accelerate_config_str = ( "\n" + "\n".join([f" - {prop}: {val}" for prop, val in accelerate_config.items()]) if isinstance(accelerate_config, dict) else accelerate_config ) commit_hash = get_git_commit_hash("trl") info = { "Platform": platform.platform(), "Python version": platform.python_version(), "TRL version": f"{__version__}+{commit_hash[:7]}" if commit_hash else __version__, "PyTorch version": version("torch"), "accelerator(s)": ", ".join(devices) if devices is not None else "cpu", "Transformers version": version("transformers"), "Accelerate version": version("accelerate"), "Accelerate config": accelerate_config_str, "Datasets version": version("datasets"), "HF Hub version": version("huggingface_hub"), "bitsandbytes version": version("bitsandbytes") if is_bitsandbytes_available() else "not installed", "DeepSpeed version": version("deepspeed") if is_deepspeed_available() else "not installed", "Liger-Kernel version": version("liger_kernel") if is_liger_kernel_available() else "not installed", "LLM-Blender version": version("llm_blender") if is_llm_blender_available() else "not installed", "OpenAI version": version("openai") if is_openai_available() else "not installed", "PEFT version": version("peft") if is_peft_available() else "not installed", "vLLM version": version("vllm") if is_vllm_available() else "not installed", } info_str = "\n".join([f"- {prop}: {val}" for prop, val in info.items()]) print(f"\nCopy-paste the following information when reporting an issue:\n\n{info_str}\n") # noqa if __name__ == "__main__": print_env() ================================================ FILE: trl/scripts/grpo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// import argparse import importlib import os import sys from dataclasses import dataclass, field from trl import ScriptArguments # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") @dataclass class GRPOScriptArguments(ScriptArguments): """ Script arguments for the GRPO training script. Args: reward_model_name_or_path (`str`, *optional*): Reward model id of a pretrained model hosted inside a model repo on huggingface.co or local path to a directory containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`]. reward_funcs (`list[str]`, *optional*): Reward functions to use. Supported values are: - `"accuracy_reward"` - `"reasoning_accuracy_reward"` - `"think_format_reward"` - `"get_soft_overlong_punishment"` (used value are `max_completion_len=1280`, `soft_punish_cache=256`) - any dotted import path " (e.g., `'my_lib.rewards.custom_reward'`). """ reward_model_name_or_path: str | None = field( default=None, metadata={ "help": "Reward model id of a pretrained model hosted inside a model repo on huggingface.co or " "local path to a directory containing model weights saved using `PreTrainedModel.save_pretrained`." }, ) reward_funcs: list[str] | None = field( default=None, metadata={ "help": "Reward functions to use. Supported values are: `accuracy_reward`, `reasoning_accuracy_reward`, `think_format_reward`, " "`get_soft_overlong_punishment` (used values are `max_completion_len=1280`, `soft_punish_cache=256`), or " "any dotted import path (e.g., `'my_lib.rewards.custom_reward'`)." }, ) def main(script_args, training_args, model_args, dataset_args): import torch from accelerate import logging from datasets import load_dataset from trl import GRPOTrainer, get_dataset, get_kbit_device_map, get_peft_config, get_quantization_config from trl.rewards import ( accuracy_reward, get_soft_overlong_punishment, reasoning_accuracy_reward, think_format_reward, ) logger = logging.get_logger(__name__) reward_funcs_registry = { "accuracy_reward": accuracy_reward, "reasoning_accuracy_reward": reasoning_accuracy_reward, "think_format_reward": think_format_reward, "get_soft_overlong_punishment": get_soft_overlong_punishment(max_completion_len=1280, soft_punish_cache=256), } # Get the reward models and functions reward_funcs = [] if script_args.reward_model_name_or_path: reward_funcs.append(script_args.reward_model_name_or_path) if script_args.reward_funcs: for func_name in script_args.reward_funcs: if func_name in reward_funcs_registry: reward_funcs.append(reward_funcs_registry[func_name]) elif "." in func_name: module_path, func_name = func_name.rsplit(".", 1) sys.path.insert(0, os.getcwd()) module = importlib.import_module(module_path) reward_func = getattr(module, func_name) reward_funcs.append(reward_func) else: raise ValueError( f"Could not load reward function '{func_name}'. Expected one of " f"{list(reward_funcs_registry.keys())} or a valid import path." ) dtype = model_args.dtype if model_args.dtype in ["auto", None] else getattr(torch, model_args.dtype) model_kwargs = dict( revision=model_args.model_revision, attn_implementation=model_args.attn_implementation, dtype=dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config training_args.model_init_kwargs = model_kwargs # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the GRPO trainer trainer = GRPOTrainer( model=model_args.model_name_or_path, reward_funcs=reward_funcs, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, GRPOConfig, ModelConfig, TrlParser dataclass_types = (GRPOScriptArguments, GRPOConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser("grpo", help="Run the GRPO training script", dataclass_types=dataclass_types) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/kto.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ Run the KTO training script with the commands below. In general, the optimal configuration for KTO will be similar to that of DPO. # Full training: ```bash python trl/scripts/kto.py \ --dataset_name trl-lib/kto-mix-14k \ --model_name_or_path=trl-lib/qwen1.5-1.8b-sft \ --per_device_train_batch_size 16 \ --num_train_epochs 1 \ --learning_rate 5e-7 \ --lr_scheduler_type=cosine \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir=kto-aligned-model \ --warmup_steps 0.1 \ --logging_first_step ``` # QLoRA: ```bash # QLoRA: python trl/scripts/kto.py \ --dataset_name trl-lib/kto-mix-14k \ --model_name_or_path=trl-lib/qwen1.5-1.8b-sft \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 5e-7 \ --lr_scheduler_type=cosine \ --gradient_accumulation_steps 1 \ --eval_steps 500 \ --output_dir=kto-aligned-model-lora \ --warmup_steps 0.1 \ --logging_first_step \ --use_peft \ --load_in_4bit \ --lora_target_modules=all-linear \ --lora_r=16 \ --lora_alpha=16 ``` """ import argparse import os # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args, dataset_args): from accelerate import logging from datasets import load_dataset from transformers import AutoModelForCausalLM, AutoTokenizer from trl import get_dataset, get_peft_config from trl.experimental.kto import KTOTrainer logger = logging.get_logger(__name__) # Load a pretrained model model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) ref_model = AutoModelForCausalLM.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) tokenizer = AutoTokenizer.from_pretrained( model_args.model_name_or_path, trust_remote_code=model_args.trust_remote_code ) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the KTO trainer trainer = KTOTrainer( model, ref_model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, processing_class=tokenizer, peft_config=get_peft_config(model_args), ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, ModelConfig, ScriptArguments, TrlParser from trl.experimental.kto import KTOConfig dataclass_types = (ScriptArguments, KTOConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser("kto", help="Run the KTO training script", dataclass_types=dataclass_types) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/reward.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// import argparse import os # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args, dataset_args): from accelerate import logging from datasets import load_dataset from trl import RewardTrainer, get_dataset, get_peft_config logger = logging.get_logger(__name__) # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the RewardTrainer trainer = RewardTrainer( model=model_args.model_name_or_path, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, ModelConfig, RewardConfig, ScriptArguments, TrlParser dataclass_types = (ScriptArguments, RewardConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser( "reward", help="Run the reward training script", dataclass_types=dataclass_types ) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/rloo.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// import argparse import importlib import os import sys from dataclasses import dataclass, field from trl import ScriptArguments # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") @dataclass class RLOOScriptArguments(ScriptArguments): """ Script arguments for the RLOO training script. Args: reward_model_name_or_path (`str`, *optional*): Reward model id of a pretrained model hosted inside a model repo on huggingface.co or local path to a directory containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`]. reward_funcs (`list[str]`, *optional*): Reward functions to use. Supported values are: - `"accuracy_reward"` - `"reasoning_accuracy_reward"` - `"think_format_reward"` - `"get_soft_overlong_punishment"` (used value are `max_completion_len=1280`, `soft_punish_cache=256`) - any dotted import path " (e.g., `'my_lib.rewards.custom_reward'`). """ reward_model_name_or_path: str | None = field( default=None, metadata={ "help": "Reward model id of a pretrained model hosted inside a model repo on huggingface.co or " "local path to a directory containing model weights saved using `PreTrainedModel.save_pretrained`." }, ) reward_funcs: list[str] | None = field( default=None, metadata={ "help": "Reward functions to use. Supported values are: `accuracy_reward`, `reasoning_accuracy_reward`, `think_format_reward`, " "`get_soft_overlong_punishment` (used values are `max_completion_len=1280`, `soft_punish_cache=256`), or " "any dotted import path (e.g., `'my_lib.rewards.custom_reward'`)." }, ) def main(script_args, training_args, model_args, dataset_args): from accelerate import logging from datasets import load_dataset from trl import RLOOTrainer, get_dataset, get_peft_config from trl.rewards import ( accuracy_reward, get_soft_overlong_punishment, reasoning_accuracy_reward, think_format_reward, ) logger = logging.get_logger(__name__) reward_funcs_registry = { "accuracy_reward": accuracy_reward, "reasoning_accuracy_reward": reasoning_accuracy_reward, "think_format_reward": think_format_reward, "get_soft_overlong_punishment": get_soft_overlong_punishment(max_completion_len=1280, soft_punish_cache=256), } # Get the reward models and functions reward_funcs = [] if script_args.reward_model_name_or_path: reward_funcs.append(script_args.reward_model_name_or_path) if script_args.reward_funcs: for func_name in script_args.reward_funcs: if func_name in reward_funcs_registry: reward_funcs.append(reward_funcs_registry[func_name]) elif "." in func_name: module_path, func_name = func_name.rsplit(".", 1) sys.path.insert(0, os.getcwd()) module = importlib.import_module(module_path) reward_func = getattr(module, func_name) reward_funcs.append(reward_func) else: raise ValueError( f"Could not load reward function '{func_name}'. Expected one of " f"{list(reward_funcs_registry.keys())} or a valid import path." ) # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the RLOO trainer trainer = RLOOTrainer( model=model_args.model_name_or_path, reward_funcs=reward_funcs, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, ModelConfig, RLOOConfig, TrlParser dataclass_types = (RLOOScriptArguments, RLOOConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser("rloo", help="Run the RLOO training script", dataclass_types=dataclass_types) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/sft.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # /// script # dependencies = [ # "trl", # "peft", # "trackio", # "kernels", # ] # /// """ # Full training ``` python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2.0e-5 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --eos_token '<|im_end|>' \ --eval_strategy steps \ --eval_steps 100 \ --output_dir Qwen2-0.5B-SFT \ --push_to_hub ``` # LoRA ``` python trl/scripts/sft.py \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2.0e-4 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --eos_token '<|im_end|>' \ --eval_strategy steps \ --eval_steps 100 \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-SFT \ --push_to_hub ``` """ import argparse import os # Enable logging in a Hugging Face Space os.environ.setdefault("TRACKIO_SPACE_ID", "trl-trackio") def main(script_args, training_args, model_args, dataset_args): from accelerate import logging from datasets import load_dataset from transformers import AutoConfig, AutoModelForCausalLM from transformers.models.auto.modeling_auto import MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES from trl import SFTTrainer, get_dataset, get_kbit_device_map, get_peft_config, get_quantization_config logger = logging.get_logger(__name__) ################ # Model init kwargs ################ model_kwargs = dict( revision=model_args.model_revision, trust_remote_code=model_args.trust_remote_code, attn_implementation=model_args.attn_implementation, dtype=model_args.dtype, ) quantization_config = get_quantization_config(model_args) if quantization_config is not None: # Passing None would not be treated the same as omitting the argument, so we include it only when valid. model_kwargs["device_map"] = get_kbit_device_map() model_kwargs["quantization_config"] = quantization_config # Create model config = AutoConfig.from_pretrained(model_args.model_name_or_path) valid_image_text_architectures = MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES.values() if config.architectures and any(arch in valid_image_text_architectures for arch in config.architectures): from transformers import AutoModelForImageTextToText model = AutoModelForImageTextToText.from_pretrained(model_args.model_name_or_path, **model_kwargs) else: model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, **model_kwargs) # Load the dataset if dataset_args.datasets and script_args.dataset_name: logger.warning( "Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the " "dataset and `dataset_name` will be ignored." ) dataset = get_dataset(dataset_args) elif dataset_args.datasets and not script_args.dataset_name: dataset = get_dataset(dataset_args) elif not dataset_args.datasets and script_args.dataset_name: dataset = load_dataset( script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming ) else: raise ValueError("Either `datasets` or `dataset_name` must be provided.") # Initialize the SFT trainer trainer = SFTTrainer( model=model, args=training_args, train_dataset=dataset[script_args.dataset_train_split], eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None, peft_config=get_peft_config(model_args), ) # Train the model trainer.train() # Log training complete trainer.accelerator.print("✅ Training completed.") # Save and push to Hub trainer.save_model(training_args.output_dir) trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.") if training_args.push_to_hub: trainer.push_to_hub(dataset_name=script_args.dataset_name) trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.") def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import DatasetMixtureConfig, ModelConfig, ScriptArguments, SFTConfig, TrlParser dataclass_types = (ScriptArguments, SFTConfig, ModelConfig, DatasetMixtureConfig) if subparsers is not None: parser = subparsers.add_parser("sft", help="Run the SFT training script", dataclass_types=dataclass_types) else: parser = TrlParser(dataclass_types, prog=prog) return parser if __name__ == "__main__": parser = make_parser() script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False) main(script_args, training_args, model_args, dataset_args) ================================================ FILE: trl/scripts/utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import importlib import inspect import logging import os import subprocess import sys from collections.abc import Iterable from dataclasses import dataclass, field from typing import TYPE_CHECKING # Temporarily import from the local module instead of transformers to avoid an upstream latency issue # See: https://github.com/huggingface/transformers/issues/44273 # This workaround can be reverted once the fix is included in the minimum required transformers version from trl.scripts._hf_argparser import DataClass, DataClassType, HfArgumentParser if TYPE_CHECKING: from datasets import DatasetDict logger = logging.getLogger(__name__) @dataclass class DatasetConfig: """ Configuration for a dataset. This class matches the signature of [`~datasets.load_dataset`] and the arguments are used directly in the [`~datasets.load_dataset`] function. You can refer to the [`~datasets.load_dataset`] documentation for more details. Parameters: path (`str`): Path or name of the dataset. name (`str`, *optional*): Defining the name of the dataset configuration. data_dir (`str`, *optional*): Defining the `data_dir` of the dataset configuration. If specified for the generic builders(csv, text etc.) or the Hub datasets and `data_files` is `None`, the behavior is equal to passing `os.path.join(data_dir, **)` as `data_files` to reference all the files in a directory. data_files (`str` or `Sequence` or `Mapping`, *optional*): Path(s) to source data file(s). split (`str`, *optional*, defaults to `"train"`): Which split of the data to load. columns (`list[str]`, *optional*): List of column names to select from the dataset. If `None`, all columns are selected. """ path: str name: str | None = None data_dir: str | None = None data_files: str | list[str] | dict[str, str] | None = None split: str = "train" columns: list[str] | None = None @dataclass class DatasetMixtureConfig: """ Configuration class for a mixture of datasets. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: datasets (`list[DatasetConfig]`): List of dataset configurations to include in the mixture. streaming (`bool`, *optional*, defaults to `False`): Whether to stream the datasets. If `True`, the datasets will be loaded in streaming mode. test_split_size (`float`, *optional*): Size of the test split. Refer to the `test_size` parameter in the [`~datasets.train_test_split`] function for more details. If `None`, the dataset will not be split into train and test sets. Usage: When using the CLI, you can add the following section to your YAML config file: ```yaml datasets: - path: ... name: ... data_dir: ... data_files: ... split: ... columns: ... - path: ... name: ... data_dir: ... data_files: ... split: ... columns: ... streaming: ... test_split_size: ... ``` """ datasets: list[DatasetConfig] = field( default_factory=list, metadata={"help": "List of dataset configurations to include in the mixture."}, ) streaming: bool = field( default=False, metadata={"help": "Whether to stream the datasets. If True, the datasets will be loaded in streaming mode."}, ) test_split_size: float | None = field( default=None, metadata={ "help": "Size of the test split. Refer to the `test_size` parameter in the `datasets.train_test_split` " "function for more details. If None, the dataset will not be split into train and test sets." }, ) def __post_init__(self): # Convert any dataset dicts (from CLI/config parsing) into DatasetConfig objects for idx, dataset in enumerate(self.datasets): if isinstance(dataset, dict): # If it's a dict, convert it to DatasetConfig self.datasets[idx] = DatasetConfig(**dataset) @dataclass class ScriptArguments: """ Arguments common to all scripts. Args: dataset_name (`str`,, *optional*): Path or name of the dataset to load. If `datasets` is provided, this will be ignored. dataset_config (`str`, *optional*): Dataset configuration name. Corresponds to the `name` argument of the [`~datasets.load_dataset`] function. If `datasets` is provided, this will be ignored. dataset_train_split (`str`, *optional*, defaults to `"train"`): Dataset split to use for training. If `datasets` is provided, this will be ignored. dataset_test_split (`str`, *optional*, defaults to `"test"`): Dataset split to use for evaluation. If `datasets` is provided, this will be ignored. dataset_streaming (`bool`, *optional*, defaults to `False`): Whether to stream the dataset. If True, the dataset will be loaded in streaming mode. If `datasets` is provided, this will be ignored. ignore_bias_buffers (`bool`, *optional*, defaults to `False`): Debug argument for distributed training. Fix for DDP issues with LM bias/mask buffers - invalid scalar type, inplace operation. See https://github.com/huggingface/transformers/issues/22482#issuecomment-1595790992. """ dataset_name: str | None = field( default=None, metadata={"help": "Path or name of the dataset to load. If `datasets` is provided, this will be ignored."}, ) dataset_config: str | None = field( default=None, metadata={ "help": "Dataset configuration name. Corresponds to the `name` argument of the `datasets.load_dataset` " "function. If `datasets` is provided, this will be ignored." }, ) dataset_train_split: str = field( default="train", metadata={"help": "Dataset split to use for training. If `datasets` is provided, this will be ignored."}, ) dataset_test_split: str = field( default="test", metadata={"help": "Dataset split to use for evaluation. If `datasets` is provided, this will be ignored."}, ) dataset_streaming: bool = field( default=False, metadata={ "help": "Whether to stream the dataset. If True, the dataset will be loaded in streaming mode. If " "`datasets` is provided, this will be ignored." }, ) ignore_bias_buffers: bool = field( default=False, metadata={ "help": "Debug argument for distributed training. Fix for DDP issues with LM bias/mask buffers - invalid " "scalar type, inplace operation. See " "https://github.com/huggingface/transformers/issues/22482#issuecomment-1595790992." }, ) def init_zero_verbose(): """ Perform zero verbose init - use this method on top of the CLI modules to make logging and warning output cleaner. Uses Rich if available, falls back otherwise. """ import logging import warnings from transformers.utils import is_rich_available FORMAT = "%(message)s" if is_rich_available(): from rich.logging import RichHandler handler = RichHandler() else: handler = logging.StreamHandler() logging.basicConfig(format=FORMAT, datefmt="[%X]", handlers=[handler], level=logging.ERROR) # Custom warning handler to redirect warnings to the logging system def warning_handler(message, category, filename, lineno, file=None, line=None): logging.warning(f"{filename}:{lineno}: {category.__name__}: {message}") # Add the custom warning handler - we need to do that before importing anything to make sure the loggers work well warnings.showwarning = warning_handler class TrlParser(HfArgumentParser): """ A subclass of [`transformers.HfArgumentParser`] designed for parsing command-line arguments with dataclass-backed configurations, while also supporting configuration file loading and environment variable management. Args: dataclass_types (`DataClassType | Iterable[DataClassType]`, *optional*): Dataclass types to use for argument parsing. **kwargs: Additional keyword arguments passed to the [`transformers.HfArgumentParser`] constructor. Examples: ```yaml # config.yaml env: VAR1: value1 arg1: 23 ``` ```python # main.py import os from dataclasses import dataclass from trl import TrlParser @dataclass class MyArguments: arg1: int arg2: str = "alpha" parser = TrlParser(dataclass_types=[MyArguments]) training_args = parser.parse_args_and_config() print(training_args, os.environ.get("VAR1")) ``` ```bash $ python main.py --config config.yaml (MyArguments(arg1=23, arg2='alpha'),) value1 $ python main.py --arg1 5 --arg2 beta (MyArguments(arg1=5, arg2='beta'),) None ``` """ def __init__( self, dataclass_types: DataClassType | Iterable[DataClassType] | None = None, **kwargs, ): # Make sure dataclass_types is an iterable if dataclass_types is None: dataclass_types = [] elif not isinstance(dataclass_types, Iterable): dataclass_types = [dataclass_types] # Check that none of the dataclasses have the "config" field for dataclass_type in dataclass_types: if "config" in dataclass_type.__dataclass_fields__: raise ValueError( f"Dataclass {dataclass_type.__name__} has a field named 'config'. This field is reserved for the " f"config file path and should not be used in the dataclass." ) super().__init__(dataclass_types=dataclass_types, **kwargs) def parse_args_and_config( self, args: Iterable[str] | None = None, return_remaining_strings: bool = False, fail_with_unknown_args: bool = True, separate_remaining_strings: bool = False, ) -> tuple[DataClass, ...]: """ Parse command-line args and config file into instances of the specified dataclass types. This method wraps [`transformers.HfArgumentParser.parse_args_into_dataclasses`] and also parses the config file specified with the `--config` flag. The config file (in YAML format) provides argument values that replace the default values in the dataclasses. Command line arguments can override values set by the config file. The method also sets any environment variables specified in the `env` field of the config file. """ import yaml args = list(args) if args is not None else sys.argv[1:] if "--config" in args: # Get the config file path from config_index = args.index("--config") args.pop(config_index) # remove the --config flag config_path = args.pop(config_index) # get the path to the config file with open(config_path) as yaml_file: config = yaml.safe_load(yaml_file) # Set the environment variables specified in the config file if "env" in config: env_vars = config.pop("env", {}) if not isinstance(env_vars, dict): raise ValueError("`env` field should be a dict in the YAML file.") for key, value in env_vars.items(): os.environ[key] = str(value) # Set the defaults from the config values config_remaining_strings = self.set_defaults_with_config(**config) else: config_remaining_strings = [] # Parse the arguments from the command line output = self.parse_args_into_dataclasses(args=args, return_remaining_strings=return_remaining_strings) # Merge remaining strings from the config file with the remaining strings from the command line if return_remaining_strings: args_remaining_strings = output[-1] if separate_remaining_strings: return output[:-1] + (config_remaining_strings, args_remaining_strings) return output[:-1] + (config_remaining_strings + args_remaining_strings,) elif fail_with_unknown_args and config_remaining_strings: raise ValueError( f"Unknown arguments from config file: {config_remaining_strings}. Please remove them, add them to the " "dataclass, or set `fail_with_unknown_args=False`." ) else: return output def set_defaults_with_config(self, **kwargs) -> list[str]: """ Overrides the parser's default values with those provided via keyword arguments, including for subparsers. Any argument with an updated default will also be marked as not required if it was previously required. Returns a list of strings that were not consumed by the parser. """ def apply_defaults(parser, kw): used_keys = set() for action in parser._actions: # Handle subparsers recursively if isinstance(action, argparse._SubParsersAction): for subparser in action.choices.values(): used_keys.update(apply_defaults(subparser, kw)) elif action.dest in kw: action.default = kw[action.dest] action.required = False used_keys.add(action.dest) return used_keys used_keys = apply_defaults(self, kwargs) # Remaining args not consumed by the parser remaining = [ item for key, value in kwargs.items() if key not in used_keys for item in (f"--{key}", str(value)) ] return remaining def get_git_commit_hash(package_name): try: # Import the package to locate its path package = importlib.import_module(package_name) # Get the path to the package using inspect package_path = os.path.dirname(inspect.getfile(package)) # Navigate up to the Git repository root if the package is inside a subdirectory git_repo_path = os.path.abspath(os.path.join(package_path, "..")) git_dir = os.path.join(git_repo_path, ".git") if os.path.isdir(git_dir): # Run the git command to get the current commit hash commit_hash = ( subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=git_repo_path).strip().decode("utf-8") ) return commit_hash else: return None except Exception as e: return f"Error: {str(e)}" def get_dataset(mixture_config: DatasetMixtureConfig) -> "DatasetDict": """ Load a mixture of datasets based on the configuration. Args: mixture_config ([`DatasetMixtureConfig`]): Script arguments containing dataset configuration. Returns: [`~datasets.DatasetDict`]: Combined dataset(s) from the mixture configuration, with optional train/test split if `test_split_size` is set. Example: ```python from trl import DatasetMixtureConfig, get_dataset from trl.scripts.utils import DatasetConfig mixture_config = DatasetMixtureConfig(datasets=[DatasetConfig(path="trl-lib/tldr")]) dataset = get_dataset(mixture_config) print(dataset) ``` ``` DatasetDict({ train: Dataset({ features: ['prompt', 'completion'], num_rows: 116722 }) }) ``` """ import datasets logger.info(f"Creating dataset mixture with {len(mixture_config.datasets)} datasets") datasets_list = [] for dataset_config in mixture_config.datasets: logger.info(f"Loading dataset for mixture: {dataset_config.path} (config name: {dataset_config.name})") dataset = datasets.load_dataset( path=dataset_config.path, name=dataset_config.name, data_dir=dataset_config.data_dir, data_files=dataset_config.data_files, split=dataset_config.split, streaming=mixture_config.streaming, ) if dataset_config.columns is not None: dataset = dataset.select_columns(dataset_config.columns) datasets_list.append(dataset) if datasets_list: combined_dataset = datasets.concatenate_datasets(datasets_list) if isinstance(combined_dataset, datasets.Dataset): # IterableDataset does not have a length logger.info(f"Created dataset mixture with {len(combined_dataset)} examples") if mixture_config.test_split_size is not None: logger.info(f"Splitting dataset into train and test sets with test size: {mixture_config.test_split_size}") combined_dataset = combined_dataset.train_test_split(test_size=mixture_config.test_split_size) return combined_dataset else: return datasets.DatasetDict({"train": combined_dataset}) else: raise ValueError("No datasets were loaded from the mixture configuration") ================================================ FILE: trl/scripts/vllm_serve.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import base64 import logging import os from collections.abc import Sequence from contextlib import asynccontextmanager from dataclasses import dataclass, field from io import BytesIO from itertools import chain from multiprocessing import Pipe, Process from multiprocessing.connection import Connection # We use CUDA with multiprocessing, so we must use the 'spawn' start method. Otherwise, we will get the following # error: RuntimeError: Cannot re-initialize CUDA in forked subprocess. To use CUDA with multiprocessing, you must use # the 'spawn' start method os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" class WeightSyncWorkerExtension: """ A vLLM worker extension that enables weight synchronization between a client and multiple server workers. This worker uses a `StatelessProcessGroup` to establish communication and a `PyNcclCommunicator` or `ProcessGroupXCCL` to handle efficient GPU-based communication using NCCL. The primary purpose of this class is to receive updated model weights from a client process and distribute them to all worker processes participating in model inference. """ # The following attributes are initialized when `init_communicator` method is called. communicator = None # Communicator for weight updates client_rank = None # Source rank for broadcasting updated weights def init_communicator(self, host: str, port: int, world_size: int, client_device_uuid: str) -> None: """ Initializes the weight update communicator using a stateless process group. This method creates a `StatelessProcessGroup` that allows external training processes to communicate with vLLM workers without interfering with the global torch distributed group. Args: host (`str`): Hostname or IP address of the master node. port (`int`): Port number to be used for communication. world_size (`int`): Total number of participating processes in the update group. client_device_uuid (`str`): UUID of the device of client main process. Used to assert that devices are different from vllm workers devices. """ import torch import torch.distributed.distributed_c10d as c10d from transformers import is_torch_xpu_available from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator from vllm.distributed.parallel_state import get_world_group from vllm.distributed.utils import StatelessProcessGroup from trl.import_utils import is_vllm_ascend_available if is_vllm_ascend_available(): from vllm_ascend.distributed.device_communicators.pyhccl import PyHcclCommunicator as PyNcclCommunicator if self.communicator is not None: raise RuntimeError("Weight update group already initialized. Call close_communicator first.") # TODO: will remove after torch xpu 2.9 support uuid in get_device_properties if torch.cuda.is_available() or ( is_torch_xpu_available() and hasattr(torch.xpu.get_device_properties(self.device), "uuid") ): accelerator_module = torch.xpu if is_torch_xpu_available() else torch.cuda if client_device_uuid == str(accelerator_module.get_device_properties(self.device).uuid): raise RuntimeError( f"Attempting to use the same CUDA device (UUID: {client_device_uuid}) for multiple distinct " "roles/ranks within the same communicator. This setup is unsupported and will likely lead to program " "hangs or incorrect behavior. Ensure that trainer is using different devices than vLLM server." ) # Get the rank of the current worker in the global world group. rank = get_world_group().rank if is_torch_xpu_available(): store = torch.distributed.TCPStore(host_name=host, port=port, world_size=world_size, is_master=(rank == 0)) prefixed_store = c10d.PrefixStore("client2server", store) xccl_options = c10d.ProcessGroupXCCL.Options() pg = c10d.ProcessGroupXCCL( store=prefixed_store, rank=rank, size=world_size, options=xccl_options, ) self.communicator = pg else: # Create a stateless process group to manage communication between training processes and vLLM workers. # Initialize the NCCL-based communicator for weight synchronization. pg = StatelessProcessGroup.create(host=host, port=port, rank=rank, world_size=world_size) self.communicator = PyNcclCommunicator(pg, device=self.device) # The client process that sends updated weights has the highest rank (world_size - 1). self.client_rank = world_size - 1 def update_named_param(self, name: str, dtype: str, shape: Sequence[int]) -> None: """ Receives updated weights from the client process and updates the named parameter in the model. Args: name (`str`): Name of the weight tensor being updated. dtype (`str`): Data type of the weight tensor as a string (e.g., `"torch.float32"`). shape (`Sequence[int]`): Shape of the weight tensor. """ import torch from transformers import is_torch_xpu_available if self.communicator is None: raise RuntimeError("Communicator not initialized. Call `init_communicator` first.") dtype = getattr(torch, dtype.split(".")[-1]) # Allocate memory for the incoming weight tensor on the correct device. weight = torch.empty(shape, dtype=dtype, device=self.device) if is_torch_xpu_available(): # Use XCCL to broadcast the updated weights from the client (src) to all workers. self.communicator.broadcast(weight, root=self.client_rank) self.communicator.barrier() else: # Use NCCL to broadcast the updated weights from the client (src) to all workers. self.communicator.broadcast(weight, src=self.client_rank) self.communicator.group.barrier() # Load the received weights into the model. self.model_runner.model.load_weights(weights=[(name, weight)]) def close_communicator(self) -> None: """ Closes the communicator when weight synchronization is no longer needed. This method deletes the NCCL communicator to release associated resources. """ if self.communicator is not None: del self.communicator self.communicator = None # Ensure attribute is reset to None self.client_rank = None # Ensure attribute is reset to None @dataclass class ScriptArguments: r""" Arguments for the script. Args: model (`str`): Model name or path to load the model from. revision (`str`, *optional*): Revision to use for the model. If not specified, the default branch will be used. tensor_parallel_size (`int`, *optional*, defaults to `1`): Number of tensor parallel workers to use. data_parallel_size (`int`, *optional*, defaults to `1`): Number of data parallel workers to use. For dense models, keep this at 1. Starting from vLLM `0.14.0`, setting this above `1` for dense models is no longer supported/useful and will error out (see vLLM PR #30739). host (`str`, *optional*, defaults to `"0.0.0.0"`): Host address to run the server on. port (`int`, *optional*, defaults to `8000`): Port to run the server on. gpu_memory_utilization (`float`, *optional*, defaults to `0.9`): Ratio (between 0 and 1) of GPU memory to reserve for the model weights, activations, and KV cache on the device dedicated to generation powered by vLLM. Higher values will increase the KV cache size and thus improve the model's throughput. However, if the value is too high, it may cause out-of-memory (OOM) errors during initialization. dtype (`str`, *optional*, defaults to `"auto"`): Data type to use for vLLM generation. If set to `"auto"`, the data type will be automatically determined based on the model configuration. Find the supported values in the vLLM documentation. max_model_len (`int`, *optional*): If set, the `max_model_len` to use for vLLM. This can be useful when running with reduced `vllm_gpu_memory_utilization`, leading to a reduced KV cache size. If not set, vLLM will use the model context size, which might be much larger than the KV cache, leading to inefficiencies. enable_prefix_caching (`bool`, *optional*): Whether to enable prefix caching in vLLM. If set to `True`, ensure that the model and the hardware support this feature. enforce_eager (`bool`, *optional*, defaults to `False`): Whether to enforce eager execution. If set to `True`, we will disable CUDA graph and always execute the model in eager mode. If `False` (default behavior), we will use CUDA graph and eager execution in hybrid. vllm_model_impl (`str`, *optional*, defaults to `"vllm"`): Model implementation to use for vLLM. Must be one of `"transformers"` or `"vllm"`. `"transformers"`: Use the `transformers` backend for model implementation. `"vllm"`: Use the `vllm` library for model implementation. kv_cache_dtype (`str`, *optional*, defaults to `"auto"`): Data type to use for KV cache. If set to `"auto"`, the dtype will default to the model data type. trust_remote_code (`bool`, *optional*, defaults to `False`): Whether to trust remote code when loading models. Set to `True` to allow executing code from model repositories. This is required for some custom models but introduces security risks. log_level (`str`, *optional*, defaults to `"info"`): Log level for uvicorn. Possible choices: `"critical"`, `"error"`, `"warning"`, `"info"`, `"debug"`, `"trace"`. """ model: str = field( metadata={"help": "Model name or path to load the model from."}, ) revision: str | None = field( default=None, metadata={"help": "Revision to use for the model. If not specified, the default branch will be used."}, ) tensor_parallel_size: int = field( default=1, metadata={"help": "Number of tensor parallel workers to use."}, ) data_parallel_size: int = field( default=1, metadata={ "help": "Number of data parallel workers to use. For dense models, keep this at 1. Starting from vLLM " "`0.14.0`, setting this above `1` for dense models is no longer supported/useful and will error out (see " "vLLM PR #30739)." }, ) host: str = field( default="0.0.0.0", metadata={"help": "Host address to run the server on."}, ) port: int = field( default=8000, metadata={"help": "Port to run the server on."}, ) gpu_memory_utilization: float = field( default=0.9, metadata={ "help": "Ratio (between 0 and 1) of GPU memory to reserve for the model weights, activations, and KV " "cache on the device dedicated to generation powered by vLLM. Higher values will increase the KV cache " "size and thus improve the model's throughput. However, if the value is too high, it may cause " "out-of-memory (OOM) errors during initialization." }, ) dtype: str = field( default="auto", metadata={ "help": "Data type to use for vLLM generation. If set to 'auto', the data type will be automatically " "determined based on the model configuration. Find the supported values in the vLLM documentation." }, ) max_model_len: int | None = field( default=None, metadata={ "help": "If set, the `max_model_len` to use for vLLM. This can be useful when running with reduced " "`vllm_gpu_memory_utilization`, leading to a reduced KV cache size. If not set, vLLM will use the model " "context size, which might be much larger than the KV cache, leading to inefficiencies." }, ) enable_prefix_caching: bool | None = field( default=None, metadata={ "help": "Whether to enable prefix caching in vLLM. If set to `True`, ensure that the model and the " "hardware support this feature." }, ) enforce_eager: bool | None = field( default=False, metadata={ "help": "Whether to enforce eager execution. If set to `True`, we will disable CUDA graph and always " "execute the model in eager mode. If `False` (default behavior), we will use CUDA graph and eager " "execution in hybrid." }, ) kv_cache_dtype: str = field( default="auto", metadata={ "help": "Data type to use for KV cache. If set to 'auto', the dtype will default to the model data type." }, ) trust_remote_code: bool = field( default=False, metadata={ "help": "Whether to trust remote code when loading models. Set to True to allow executing code from model " "repositories. This is required for some custom models but introduces security risks." }, ) log_level: str = field( default="info", metadata={ "help": "Log level for uvicorn. Possible choices: 'critical', 'error', 'warning', 'info', 'debug', " "'trace'." }, ) vllm_model_impl: str = field( default="vllm", metadata={ "help": "Model implementation to use for vLLM. Must be one of `transformers` or `vllm`. `transformers`: " "Use the `transformers` backend for model implementation. `vllm`: Use the `vllm` library for " "model implementation." }, ) def llm_worker( script_args: ScriptArguments, data_parallel_rank: int, master_port: int, connection: Connection ) -> None: from vllm import LLM # Set required environment variables for DP to work with vLLM os.environ["VLLM_DP_RANK"] = str(data_parallel_rank) os.environ["VLLM_DP_RANK_LOCAL"] = str(data_parallel_rank) os.environ["VLLM_DP_SIZE"] = str(script_args.data_parallel_size) os.environ["VLLM_DP_MASTER_PORT"] = str(master_port) llm = LLM( model=script_args.model, revision=script_args.revision, tensor_parallel_size=script_args.tensor_parallel_size, gpu_memory_utilization=script_args.gpu_memory_utilization, enforce_eager=script_args.enforce_eager, dtype=script_args.dtype, # Automatic Prefix Caching caches the KV cache of existing queries, so that a new query can # directly reuse the KV cache if it shares the same prefix with one of the existing queries. # This is particularly useful here because we generate completions from the same prompts. enable_prefix_caching=script_args.enable_prefix_caching, kv_cache_dtype=script_args.kv_cache_dtype, max_model_len=script_args.max_model_len, worker_extension_cls="trl.scripts.vllm_serve.WeightSyncWorkerExtension", trust_remote_code=script_args.trust_remote_code, model_impl=script_args.vllm_model_impl, # Important so temperature scaling/logit tweaking affects the TIS log probs logprobs_mode="processed_logprobs", ) # Send ready signal to parent process connection.send({"status": "ready"}) while True: # Wait for commands from the parent process try: command = connection.recv() except KeyboardInterrupt: llm.collective_rpc(method="close_communicator") break # Handle commands if command["type"] in ["call", "fire_and_forget"]: method_name = command["method"] args, kwargs = command.get("args", ()), command.get("kwargs", {}) method = getattr(llm, method_name) result = method(*args, **kwargs) if command["type"] == "call": connection.send(result) elif command["type"] == "shutdown": break def chunk_list(lst: list, n: int) -> list[list]: """ Split list `lst` into `n` evenly distributed sublists. Example: ```python >>> chunk_list([1, 2, 3, 4, 5, 6], 2) [[1, 2, 3], [4, 5, 6]] >>> chunk_list([1, 2, 3, 4, 5, 6], 4) [[1, 2], [3, 4], [5], [6]] >>> chunk_list([1, 2, 3, 4, 5, 6], 8) [[1], [2], [3], [4], [5], [6], [], []] ``` """ k, r = divmod(len(lst), n) return [lst[i * k + min(i, r) : (i + 1) * k + min(i + 1, r)] for i in range(n)] def main(script_args: ScriptArguments): from packaging.version import Version from transformers import is_vision_available from trl.generation.vllm_generation import extract_logprobs from trl.import_utils import ( is_fastapi_available, is_pydantic_available, is_uvicorn_available, is_vllm_available, ) if not is_fastapi_available(): raise ImportError( "FastAPI is required to run the vLLM serve script. Please install it using `pip install fastapi`." ) if not is_pydantic_available(): raise ImportError( "Pydantic is required to run the vLLM serve script. Please install it using `pip install pydantic`." ) if not is_uvicorn_available(): raise ImportError( "Uvicorn is required to run the vLLM serve script. Please install it using `pip install uvicorn`." ) if not is_vllm_available(): raise ImportError("vLLM is required to run the vLLM serve script. Please install it using `pip install vllm`.") import uvicorn import vllm from fastapi import FastAPI from pydantic import BaseModel from vllm import SamplingParams if Version(vllm.__version__) <= Version("0.11.0"): from vllm.utils import get_open_port else: from vllm.utils.network_utils import get_open_port if is_vision_available(): from PIL import Image logger = logging.getLogger(__name__) # Spawn dp workers, and setup pipes for communication master_port = get_open_port() connections = [] processes = [] for data_parallel_rank in range(script_args.data_parallel_size): parent_connection, child_connection = Pipe() process = Process(target=llm_worker, args=(script_args, data_parallel_rank, master_port, child_connection)) process.start() connections.append(parent_connection) processes.append(process) @asynccontextmanager async def lifespan(app: FastAPI): # Wait for all workers to send "ready" ready_connections = set() while len(ready_connections) < script_args.data_parallel_size: for connection in connections: msg = connection.recv() if isinstance(msg, dict) and msg.get("status") == "ready": ready_connections.add(connection) yield # Wait for processes to terminate for process in processes: process.join(timeout=10) # Wait for 10 seconds for the process to terminate if process.is_alive(): logger.warning(f"Process {process} is still alive after 10 seconds, attempting to terminate...") process.terminate() process.join() # ensure process termination after calling terminate() app = FastAPI(lifespan=lifespan) # Define the endpoints for the model server @app.get("/health/") async def health(): """ Health check endpoint to verify that the server is running. """ return {"status": "ok"} @app.get("/get_world_size/") async def get_world_size(): """ Retrieves the world size of the LLM engine, which is `tensor_parallel_size * data_parallel_size`. Returns: `dict`: A dictionary containing the world size. Example response: ```json {"world_size": 8} ``` """ return {"world_size": script_args.tensor_parallel_size * script_args.data_parallel_size} class GenerateRequest(BaseModel): prompts: list[str] | list[list[int]] images: list[list[str] | None] | None = None n: int = 1 repetition_penalty: float = 1.0 temperature: float = 1.0 top_p: float = 1.0 top_k: int = -1 min_p: float = 0.0 max_tokens: int = 16 logprobs: int | None = 0 structured_outputs_regex: str | None = None generation_kwargs: dict = field(default_factory=dict) class GenerateResponse(BaseModel): prompt_ids: list[list[int]] completion_ids: list[list[int]] logprobs: list[list[list[float | None]]] | None logprob_token_ids: list[list[list[int]]] | None @app.post("/generate/", response_model=GenerateResponse) async def generate(request: GenerateRequest): """ Generates completions for the provided prompts. Args: request (`GenerateRequest`): - `prompts` (list of `str` or list of list of `int`): A list of prompts. It accepts either text strings or pre-tokenized token ID lists. When text strings are provided, `images` can optionally be included. - `images` (list of list of `str` or `None`, *optional*): A list of image lists. Each element is a list of base64-encoded images for the corresponding prompt, or `None` if no images for that prompt. - `n` (`int`, *optional*, defaults to `1`): Number of completions to generate for each prompt. - `repetition_penalty` (`float`, *optional*, defaults to `1.0`): Repetition penalty to apply during generation. - `temperature` (`float`, *optional*, defaults to `1.0`): Temperature for sampling. Higher values lead to more random outputs. - `top_p` (`float`, *optional*, defaults to `1.0`): Top-p (nucleus) sampling parameter. It controls the diversity of the generated text. - `top_k` (`int`, *optional*, defaults to `-1`): Top-k sampling parameter. If set to `-1`, it disables top-k sampling. - `min_p` (`float`, *optional*, defaults to `0.0`): Minimum probability threshold for sampling. - `max_tokens` (`int`, *optional*, defaults to `16`): Maximum number of tokens to generate for each completion. - `logprobs` (`int`, *optional*, defaults to `0`): Number of top logprobs to return per token. When 0, only the sampled token's logprob is returned. When N>0, returns up to N+1 logprobs sorted by descending probability, because vLLM always includes the sampled token's logprob (which may fall outside the top-N). - `structured_outputs_regex` (`str`, *optional*): A regex pattern for structured outputs. If provided, the model will only generate tokens that match this regex pattern. - `generation_kwargs` (`dict`, *optional*): Additional generation parameters to pass to the vLLM `SamplingParams`. This can include parameters like `seed`, `frequency_penalty`, etc. If it contains keys that conflict with the other parameters, they will override them. Returns: `GenerateResponse`: - `prompt_ids` (list of list of `int`): A list of lists of token IDs for each input prompt. - `completion_ids` (list of list of `int`): A list of lists of token IDs for each generated completion. - `logprobs` (list of list of list of `float`): Per-token logprobs of shape (num_sequences, seq_len, num_logprobs), sorted by descending probability. - `logprob_token_ids` (list of list of list of `int`): Token IDs corresponding to each logprob, same shape as `logprobs`. Example request (text prompts): ```json {"prompts": ["Hello world", "What is AI?"]} ``` Example request (token IDs): ```json {"prompts": [[101, 102], [201, 202]]} ``` Example response: ```json { "prompt_ids": [[101, 102], [201, 202]], "completion_ids": [[103, 104, 105], [203, 204, 205]], "logprobs": [[[-0.1], [-0.2], [-0.3]], [[-0.4], [-0.5], [-0.6]]], "logprob_token_ids": [[[103], [104], [105]], [[203], [204], [205]]] } ``` """ # Build vLLM-compatible prompt inputs is_token_ids = request.prompts and isinstance(request.prompts[0], list) request.images = request.images or [None] * len(request.prompts) prompts = [] for prompt, image_list in zip(request.prompts, request.images, strict=True): row = {"prompt_token_ids": prompt} if is_token_ids else {"prompt": prompt} if image_list is not None: row["multi_modal_data"] = {"image": [Image.open(BytesIO(base64.b64decode(img))) for img in image_list]} prompts.append(row) generation_kwargs = { "n": request.n, "repetition_penalty": request.repetition_penalty, "temperature": request.temperature, "top_p": request.top_p, "top_k": request.top_k, "min_p": request.min_p, "max_tokens": request.max_tokens, "logprobs": request.logprobs, } generation_kwargs.update(request.generation_kwargs) # Structured outputs, if enabled if Version(vllm.__version__) <= Version("0.10.2"): from vllm.sampling_params import GuidedDecodingParams as StructuredOutputsParams structured_outputs_key = "guided_decoding" else: from vllm.sampling_params import StructuredOutputsParams structured_outputs_key = "structured_outputs" if request.structured_outputs_regex is not None: if generation_kwargs.get(structured_outputs_key) is not None: logger.warning( f"Both `structured_outputs_regex` and `generation_kwargs['{structured_outputs_key}']` are set; " "`structured_outputs_regex` takes precedence." ) generation_kwargs[structured_outputs_key] = StructuredOutputsParams(regex=request.structured_outputs_regex) elif isinstance(structured_outputs_kwargs := generation_kwargs.get(structured_outputs_key), dict): generation_kwargs[structured_outputs_key] = StructuredOutputsParams(**structured_outputs_kwargs) sampling_params = SamplingParams(**generation_kwargs) # Evenly distribute prompts across DP ranks chunked_prompts = chunk_list(prompts, script_args.data_parallel_size) # Send the prompts to each worker for connection, prompts in zip(connections, chunked_prompts, strict=True): # When the number of prompts is less than data_parallel_size, some workers will receive empty prompts. # However, vLLM requires that we always send at least one prompt. So we send a placeholder prompt to comply # with vLLM's requirement, and we later ignore the result. if not prompts: prompts = [""] kwargs = {"prompts": prompts, "sampling_params": sampling_params} connection.send({"type": "call", "method": "generate", "kwargs": kwargs}) # Receive results all_outputs = [connection.recv() for connection in connections] # Handle empty prompts (see above) all_outputs = [output for output, prompts in zip(all_outputs, chunked_prompts, strict=True) if prompts] # Flatten and combine all results all_outputs = list(chain.from_iterable(all_outputs)) # from list of list to single list prompt_ids = [output.prompt_token_ids for output in all_outputs] completion_ids = [list(output.token_ids) for outputs in all_outputs for output in outputs.outputs] logprobs, logprob_token_ids = extract_logprobs(all_outputs) return { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "logprob_token_ids": logprob_token_ids, } class ChatRequest(BaseModel): messages: list[list[dict]] n: int = 1 repetition_penalty: float = 1.0 temperature: float = 1.0 top_p: float = 1.0 top_k: int = -1 min_p: float = 0.0 max_tokens: int = 16 logprobs: int | None = 0 structured_outputs_regex: str | None = None generation_kwargs: dict = field(default_factory=dict) chat_template_kwargs: dict = field(default_factory=dict) tools: list | None = None class ChatResponse(BaseModel): prompt_ids: list[list[int]] completion_ids: list[list[int]] logprobs: list[list[list[float | None]]] | None logprob_token_ids: list[list[list[int]]] | None @app.post("/chat/", response_model=ChatResponse) async def chat(request: ChatRequest): """ Generates completions for the provided chat messages. Args: request (`ChatRequest`): - `messages` (list of `dict`): A list of messages (dicts with "role" and "content" keys) for the model to generate completions. - `n` (`int`, *optional*, defaults to `1`): Number of completions to generate for each prompt. - `repetition_penalty` (`float`, *optional*, defaults to `1.0`): Repetition penalty to apply during generation. - `temperature` (`float`, *optional*, defaults to `1.0`): Temperature for sampling. Higher values lead to more random outputs. - `top_p` (`float`, *optional*, defaults to `1.0`): Top-p (nucleus) sampling parameter. It controls the diversity of the generated text. - `top_k` (`int`, *optional*, defaults to `-1`): Top-k sampling parameter. If set to `-1`, it disables top-k sampling. - `min_p` (`float`, *optional*, defaults to `0.0`): Minimum probability threshold for sampling. - `max_tokens` (`int`, *optional*, defaults to `16`): Maximum number of tokens to generate for each completion. - `logprobs` (`int`, *optional*, defaults to `0`): Number of top logprobs to return per token. When 0, only the sampled token's logprob is returned. When N>0, returns up to N+1 logprobs sorted by descending probability, because vLLM always includes the sampled token's logprob (which may fall outside the top-N). - `structured_outputs_regex` (`str`, *optional*): A regex pattern for structured outputs. If provided, the model will only generate tokens that match this regex pattern. - `generation_kwargs` (`dict`, *optional*): Additional generation parameters to pass to the vLLM `SamplingParams`. This can include parameters like `seed`, `frequency_penalty`, etc. If it contains keys that conflict with the other parameters, they will override them. - `chat_template_kwargs` (`dict`, *optional*): Additional keyword arguments to pass to the chat template. Returns: `ChatResponse`: - `prompt_ids` (list of list of `int`): A list of lists of token IDs for each input prompt. - `completion_ids` (list of list of `int`): A list of lists of token IDs for each generated completion. - `logprobs` (list of list of list of `float`): Per-token logprobs of shape (num_sequences, seq_len, num_logprobs), sorted by descending probability. - `logprob_token_ids` (list of list of list of `int`): Token IDs corresponding to each logprob, same shape as `logprobs`. Example request: ```bash curl -X POST 'http://0.0.0.0:8000/chat/' \ -H 'Content-Type: application/json' \ -d '{"messages": [[{ "role": "user", "content": "Hello!" }]]}' ``` Example response: ```json { "prompt_ids": [[151644, 872, 198, 9707, 0, 151645, 198, 151644, 77091, 198]], "completion_ids": [[151667, 198, 32313, 11, 279]], "logprobs": [[[-0.0003], [-3.58e-07], [-0.0902], [-6.39e-05], [-0.0387]]], "logprob_token_ids": [[[151667], [198], [32313], [11], [279]]] } ``` """ # Convert PIL images to base64 strings for message_list in request.messages: for message in message_list: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image_pil": part["image_pil"] = Image.open(BytesIO(base64.b64decode(part["image_pil"]))) generation_kwargs = { "n": request.n, "repetition_penalty": request.repetition_penalty, "temperature": request.temperature, "top_p": request.top_p, "top_k": request.top_k, "min_p": request.min_p, "max_tokens": request.max_tokens, "logprobs": request.logprobs, } generation_kwargs.update(request.generation_kwargs) # Structured outputs, if enabled if Version(vllm.__version__) <= Version("0.10.2"): from vllm.sampling_params import GuidedDecodingParams as StructuredOutputsParams structured_outputs_key = "guided_decoding" else: from vllm.sampling_params import StructuredOutputsParams structured_outputs_key = "structured_outputs" if request.structured_outputs_regex is not None: if generation_kwargs.get(structured_outputs_key) is not None: logger.warning( f"Both `structured_outputs_regex` and `generation_kwargs['{structured_outputs_key}']` are set; " "`structured_outputs_regex` takes precedence." ) generation_kwargs[structured_outputs_key] = StructuredOutputsParams(regex=request.structured_outputs_regex) elif isinstance(structured_outputs_kwargs := generation_kwargs.get(structured_outputs_key), dict): generation_kwargs[structured_outputs_key] = StructuredOutputsParams(**structured_outputs_kwargs) sampling_params = SamplingParams(**generation_kwargs) # Evenly distribute prompts across DP ranks chunked_messages = chunk_list(request.messages, script_args.data_parallel_size) # Send the messages to each worker for connection, messages in zip(connections, chunked_messages, strict=True): # When the number of messages is less than data_parallel_size, some workers will receive empty messages. # However, vLLM requires that we always send at least one prompt. So we send a placeholder prompt to comply # with vLLM's requirement, and we later ignore the result. if not messages: messages = [[{"role": "user", "content": ""}]] kwargs = { "messages": messages, "sampling_params": sampling_params, "chat_template_kwargs": request.chat_template_kwargs, "tools": request.tools, } connection.send({"type": "call", "method": "chat", "kwargs": kwargs}) # Receive results all_outputs = [connection.recv() for connection in connections] # Handle empty prompts (see above) all_outputs = [output for output, prompts in zip(all_outputs, chunked_messages, strict=True) if prompts] # Flatten and combine all results all_outputs = list(chain.from_iterable(all_outputs)) # from list of list to single list prompt_ids = [output.prompt_token_ids for output in all_outputs] completion_ids = [list(output.token_ids) for outputs in all_outputs for output in outputs.outputs] logprobs, logprob_token_ids = extract_logprobs(all_outputs) return { "prompt_ids": prompt_ids, "completion_ids": completion_ids, "logprobs": logprobs, "logprob_token_ids": logprob_token_ids, } class InitCommunicatorRequest(BaseModel): host: str port: int world_size: int client_device_uuid: str @app.post("/init_communicator/") async def init_communicator(request: InitCommunicatorRequest): """ Initializes the communicator for synchronizing model weights between a client and multiple server workers. Args: request (`InitCommunicatorRequest`): - `host` (`str`): Hostname or IP address of the master node. - `port` (`int`): Port number to be used for communication. - `world_size` (`int`): Total number of participating processes in the group. - `client_device_uuid` (`str`): UUID of the device of client main process. Used to assert that devices are different from vLLM workers devices. """ world_size = script_args.tensor_parallel_size * script_args.data_parallel_size + 1 # The function init_communicator is called this way: init_communicator(host, port, world_size) # So with collective_rpc we need to call it this way: # llm.collective_rpc(method="init_communicator", args=(host, port, world_size)) kwargs = { "method": "init_communicator", "args": (request.host, request.port, world_size, request.client_device_uuid), } for connection in connections: connection.send({"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs}) return {"message": "Request received, initializing communicator"} class UpdateWeightsRequest(BaseModel): name: str dtype: str shape: list[int] @app.post("/update_named_param/") async def update_named_param(request: UpdateWeightsRequest): """ Updates the model weights with the provided tensor. Once this endpoint is called, the client process should broadcast the updated weights to all server workers. Args: request (`UpdateWeightsRequest`): - `name` (`str`): Name of the weight tensor being updated. - `dtype` (`str`): Data type of the weight tensor (e.g., `"torch.float32"`). - `shape` (list of `int`): Shape of the weight """ # The function update_named_param is called this way: update_named_param("name", "torch.float32", (10, 10)) # So with collective_rpc we need to call it this way: # llm.collective_rpc("update_named_param", args=("name", "torch.float32", (10, 10))) kwargs = {"method": "update_named_param", "args": (request.name, request.dtype, tuple(request.shape))} for connection in connections: connection.send({"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs}) return {"message": "Request received, updating named parameter"} @app.post("/reset_prefix_cache/") async def reset_prefix_cache(): """ Resets the prefix cache for the model. """ for connection in connections: connection.send({"type": "call", "method": "reset_prefix_cache"}) # Wait for and collect all results all_outputs = [connection.recv() for connection in connections] success = all(output for output in all_outputs) return {"message": "Request received, resetting prefix cache status: " + str(success)} @app.post("/close_communicator/") async def close_communicator(): """ Closes the weight update group and cleans up associated resources. """ kwargs = {"method": "close_communicator"} for connection in connections: connection.send({"type": "fire_and_forget", "method": "collective_rpc", "kwargs": kwargs}) return {"message": "Request received, closing communicator"} # Start the server uvicorn.run(app, host=script_args.host, port=script_args.port, log_level=script_args.log_level) def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None): from trl import TrlParser if subparsers is not None: parser = subparsers.add_parser("vllm-serve", help="Run the vLLM serve script", dataclass_types=ScriptArguments) else: parser = TrlParser(ScriptArguments, prog=prog) return parser if __name__ == "__main__": parser = make_parser() (script_args,) = parser.parse_args_and_config() main(script_args) ================================================ FILE: trl/skills/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .skills import ( install_skill, list_agent_names, list_skills, resolve_target_path, uninstall_skill, ) ================================================ FILE: trl/skills/cli.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ CLI commands for TRL skills installation and management. This module provides command-line interface for installing TRL skills to various AI agent directories. """ import argparse from .skills import install_skill, list_agent_names, list_skills, resolve_target_path, uninstall_skill def add_skills_subcommands(subparsers: argparse._SubParsersAction) -> None: """ Add skills subcommands to the parser. This creates nested subcommands under 'trl skills' for managing skill installations. Args: subparsers: Subparsers from 'trl skills' command """ # Parent parser for common target options target_parser = argparse.ArgumentParser(add_help=False) target_parser.add_argument( "--target", required=True, help=f"Installation target: agent name ({', '.join(list_agent_names())}) or directory path", ) target_parser.add_argument( "--scope", choices=["project", "global"], default="project", help="Scope when using --target with agent name: project (./agent/skills/) or global (user-level like ~/.agent/skills/)", ) # trl skills list (no target required - lists TRL's built-in skills by default) list_parser = subparsers.add_parser( "list", help="List available TRL skills or installed skills in a target", description="Show TRL skills available for installation, or if --target is specified, show installed skills", ) list_parser.add_argument( "--target", help="Optional: show installed skills in target (agent name or directory path)", ) list_parser.add_argument( "--scope", choices=["project", "global"], default="project", help="Scope when using --target with agent name: project (./agent/skills/) or global (user-level like ~/.agent/skills/)", ) list_parser.set_defaults(func=cmd_list) # trl skills install install_parser = subparsers.add_parser( "install", parents=[target_parser], help="Install skill", description="Install TRL skill to to target", ) install_parser.add_argument("skill", nargs="?", help="Skill name to install (omit to use --all)") install_parser.add_argument("--all", action="store_true", help="Install all available TRL skills") install_parser.add_argument("--force", action="store_true", help="Overwrite if skill already exists") install_parser.set_defaults(func=cmd_install) # trl skills uninstall uninstall_parser = subparsers.add_parser( "uninstall", parents=[target_parser], help="Uninstall skill from target", description="Remove a TRL skill from an AI agent's skills directory", ) uninstall_parser.add_argument("skill", help="Skill name to uninstall") uninstall_parser.set_defaults(func=cmd_uninstall) def cmd_install(args): """Handle 'trl skills install' command.""" # Validate arguments if not args.skill and not args.all: print("Error: Either provide a skill name or use --all to install all skills") print("Usage: trl skills install --target ") print(" or: trl skills install --all --target ") return 1 if args.skill and args.all: print("Error: Cannot specify both a skill name and --all") return 1 # Determine skills to install if args.all: skills_to_install = list_skills() if not skills_to_install: print("No skills available to install") return 1 print(f"Installing {len(skills_to_install)} skills to {args.target}") else: skills_to_install = [args.skill] # Install each skill success_count = 0 for skill_name in skills_to_install: try: print(f"Installing '{skill_name}'...", end=" ") install_skill( skill_name=skill_name, target=args.target, scope=args.scope, force=args.force, ) print("✓") success_count += 1 except FileExistsError as e: print("✗") print(f" Error: {e}") if not args.force: print(" Use --force to overwrite") except (FileNotFoundError, ValueError) as e: print("✗") print(f" Error: {e}") # Summary print(f"\n{success_count}/{len(skills_to_install)} skills installed successfully") if success_count > 0: target_path = resolve_target_path(args.target, args.scope) print(f"\nSkills are now available at: {target_path}") print("You may need to restart your AI agent to use the new skills.") return 0 if success_count == len(skills_to_install) else 1 def cmd_uninstall(args): """Handle 'trl skills uninstall' command.""" try: print(f"Uninstalling '{args.skill}' from {args.target}...", end=" ") uninstall_skill(args.skill, target=args.target, scope=args.scope) print("✓") print(f"\nSkill '{args.skill}' has been removed") return 0 except (FileNotFoundError, PermissionError, ValueError) as e: print("✗") print(f"Error: {e}") return 1 def cmd_list(args): """Handle 'trl skills list' command.""" try: # List skills - if no target specified, list TRL's built-in skills if args.target: skills = list_skills(target=args.target, scope=args.scope) location = args.target else: skills = list_skills() location = "TRL (available for installation)" if not skills: if args.target: print(f"No skills installed in {args.target}") else: print("No TRL skills available") return 0 print(f"\nSkills in {location}:\n") for skill in skills: print(f" {skill}") print(f"\nTotal: {len(skills)} skill(s)") if not args.target: print("\nUse 'trl skills install --target ' to install a skill") return 0 except ValueError as e: print(f"Error: {e}") return 1 __all__ = [ "add_skills_subcommands", ] ================================================ FILE: trl/skills/skills.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ Agent Skills. This module: - provides utilities for discovering and accessing TRL skills that can be used by AI agents to learn how to use the TRL CLI - handles installation, uninstallation, and management of TRL skills - defines where different AI agents and coding tools look for skills, enabling easy installation of TRL skills to the appropriate directories Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to perform tasks more accurately and efficiently. Learn more at https://agentskills.io """ import importlib.resources as resources import shutil from pathlib import Path AGENT_PATHS = { "claude": { "global": Path("~/.claude/skills"), "project": Path("./.claude/skills"), }, "codex": { "global": Path("~/.codex/skills"), "project": Path("./.codex/skills"), }, "opencode": { "global": Path("~/.config/opencode/skills"), "project": Path(".opencode/skills"), }, } def list_agent_names() -> list[str]: """ List available predefined agent names. Returns: `list[str]`: Sorted list of agent names (e.g., ['claude', 'codex', 'opencode']). """ return sorted(AGENT_PATHS.keys()) def _get_trl_skills_dir() -> Path: """ Get the path to the TRL skills directory. This is the directory inside the TRL package containing skills that can be installed to AI agent directories. Returns: `Path`: TRL skills directory. """ return Path(str(resources.files("trl.skills"))) def resolve_target_path(target: str | Path, scope: str = "project") -> Path: """ Resolve target to a concrete directory path. Converts semantic agent names (e.g., 'claude') with scope to actual filesystem paths, or normalizes provided paths. Args: target (`str | Path`): Agent name (e.g., 'claude', 'codex') or directory path. scope (`str`, defaults to `"project"`): Scope for agent names: 'global' (user-level like ~/.agent/skills/) or 'project' (./agent/skills/). Returns: `Path`: Resolved absolute path. Raises: `ValueError`: If `scope` is invalid for a predefined agent target. Example: ```python from trl.skills import resolve_target_path # Resolve agent name with scope path = resolve_target_path("claude", "global") print(path) # /home/user/.claude/skills # Resolve custom path path = resolve_target_path("/custom/skills") print(path) # /custom/skills ``` """ if isinstance(target, Path): return target.expanduser().resolve() # Check if it's a predefined agent if target in AGENT_PATHS: if scope not in AGENT_PATHS[target]: valid_scopes = ", ".join(sorted(AGENT_PATHS[target])) raise ValueError(f"Invalid scope '{scope}' for agent '{target}'. Expected one of: {valid_scopes}") agent_path = AGENT_PATHS[target][scope] return agent_path.expanduser().resolve() # Treat as custom path string return Path(target).expanduser().resolve() def _list_skills_in_dir(skills_dir: Path) -> list[str]: """ List skills in directory. A skill is a directory containing a SKILL.md file. Args: skills_dir (`Path`): Skills directory to scan. Returns: `list[str]`: Skill names (directory names containing SKILL.md). """ if not skills_dir.exists(): return [] skills = [] for item in skills_dir.iterdir(): if item.is_dir() and (item / "SKILL.md").exists(): skills.append(item.name) return sorted(skills) def list_skills(target: str | Path | None = None, scope: str = "project") -> list[str]: """ List skills. A skill is a directory containing a SKILL.md file. Args: target (`str | Path`, *optional*): Agent name (e.g., 'claude'), directory path, or `None` for TRL's built-in skills. scope (`str`, defaults to `"project"`): For agent names: 'global' (user-level) or 'project' (current directory). Returns: `list[str]`: Skill names (directory names containing SKILL.md). Example: ```python from trl.skills import list_skills # List TRL's built-in skills skills = list_skills() print(skills) # ['trl-training'] # List skills installed for Claude globally installed = list_skills(target="claude", scope="global") print(installed) # ['trl-training', 'custom-skill'] # List skills in custom directory custom = list_skills(target="/path/to/skills") print(custom) # [...] ``` """ if target is None: # List TRL's built-in skills return _list_skills_in_dir(_get_trl_skills_dir()) target_dir = resolve_target_path(target, scope) return _list_skills_in_dir(target_dir) def _install_skill_to_dir( skill_name: str, target_dir: Path, source_dir: Path, force: bool = False, ) -> bool: """ Install a skill to target directory. Args: skill_name (`str`): Name of skill to install. target_dir (`Path`): Target installation directory. source_dir (`Path`): Source directory containing skills. force (`bool`, defaults to `False`): Whether to overwrite if exists. Returns: `bool`: True if installed successfully. Raises: - `FileNotFoundError`: If skill doesn't exist in source_dir. - `FileExistsError`: If skill already installed and not force. - `PermissionError`: If no permission to write to target_dir. - `ValueError`: If source_dir entry exists but is not a directory. - `OSError`: If copying the skill fails. """ source_skill = source_dir / skill_name # Check if source skill exists if not source_skill.exists(): available = ", ".join(list_skills(target=source_dir)) source_msg = f"source directory {source_dir}" if available: raise FileNotFoundError(f"Skill '{skill_name}' not found in {source_msg}. Available skills: {available}") raise FileNotFoundError(f"Skill '{skill_name}' not found in {source_msg}") if not source_skill.is_dir(): raise ValueError(f"Skill '{skill_name}' is not a directory") target_skill = target_dir / skill_name # Check if already exists if target_skill.exists() and not force: raise FileExistsError(f"Skill '{skill_name}' already installed at {target_skill}. Use --force to overwrite.") # Create target directory try: target_dir.mkdir(parents=True, exist_ok=True) except PermissionError as e: raise PermissionError(f"Cannot create directory {target_dir}: {e}") from e # Remove existing if force if target_skill.exists() and force: if target_skill.is_symlink(): target_skill.unlink() else: shutil.rmtree(target_skill) # Install try: shutil.copytree(source_skill, target_skill) except OSError as e: raise OSError(f"Failed to install skill: {e}") from e return True def install_skill( skill_name: str, target: str | Path, scope: str = "project", source: str | Path | None = None, force: bool = False, ) -> bool: """ Install a skill. Args: skill_name (`str`): Name of skill to install. target (`str | Path`): Agent name (e.g., 'claude', 'codex') or directory path. scope (`str`, defaults to `"project"`): Scope for agent names: 'global' (user-level) or 'project' (current directory). source (`str | Path`, *optional*): Source directory containing skills. If `None`, defaults to TRL skills directory. force (`bool`, defaults to `False`): Whether to overwrite if skill already exists. Returns: `bool`: True if installed successfully. Raises: - `FileNotFoundError`: If skill doesn't exist in source. - `FileExistsError`: If skill already installed and not force. - `PermissionError`: If no permission to write to target. - `ValueError`: - If `scope` is invalid for a predefined agent target. - If `source` entry exists but is not a directory. - `OSError`: If copying the skill fails. Example: ```python from trl.skills import install_skill # Install to Claude's global skills directory install_skill("trl-training", target="claude", scope="global") # Install to custom directory install_skill("trl-training", target="/path/to/skills") # Overwrite existing installation install_skill("trl-training", target="claude", force=True) ``` """ target_dir = resolve_target_path(target, scope) source_dir = Path(source).expanduser().resolve() if source else _get_trl_skills_dir() return _install_skill_to_dir(skill_name, target_dir, source_dir, force) def _uninstall_skill_from_dir(skill_name: str, target_dir: Path) -> bool: """ Uninstall a skill from target directory. Args: skill_name (`str`): Name of skill to uninstall. target_dir (`Path`): Directory skill is installed in. Returns: `bool`: True if uninstalled successfully. Raises: - `FileNotFoundError`: If skill not installed. - `PermissionError`: If no permission to remove. - `OSError`: If removing the skill fails for another filesystem reason. """ target_skill = target_dir / skill_name if not target_skill.exists(): raise FileNotFoundError(f"Skill '{skill_name}' not installed at {target_dir}") # Remove symlink or directory try: shutil.rmtree(target_skill) except PermissionError as e: raise PermissionError(f"Cannot remove skill: {e}") from e except OSError as e: raise OSError(f"Failed to remove skill: {e}") from e return True def uninstall_skill(skill_name: str, target: str | Path, scope: str = "project") -> bool: """ Uninstall a skill. Args: skill_name (`str`): Name of skill to uninstall. target (`str | Path`): Agent name (e.g., 'claude', 'codex') or directory path. scope (`str`, defaults to `"project"`): Scope for agent names: 'global' (user-level) or 'project' (current directory). Returns: `bool`: True if uninstalled successfully. Raises: - `FileNotFoundError`: If skill not installed. - `PermissionError`: If no permission to remove. - `OSError`: If removing the skill fails for another filesystem reason. - `ValueError`: If `scope` is invalid for a predefined agent target. Example: ```python from trl.skills import uninstall_skill # Uninstall from Claude's global directory uninstall_skill("trl-training", target="claude", scope="global") # Uninstall from custom directory uninstall_skill("trl-training", target="/path/to/skills") ``` """ target_dir = resolve_target_path(target, scope) return _uninstall_skill_from_dir(skill_name, target_dir) ================================================ FILE: trl/skills/trl-training/SKILL.md ================================================ --- name: trl-training description: Train and fine-tune transformer language models using TRL (Transformers Reinforcement Learning). Supports SFT, DPO, GRPO, KTO, RLOO and Reward Model training via CLI commands. license: Apache-2.0 metadata: version: "1.0.0" author: huggingface commands: - trl sft - trl dpo - trl grpo - trl kto - trl rloo - trl reward categories: - machine-learning - llm-training - reinforcement-learning tags: - rlhf - supervised-fine-tuning - dpo - grpo - huggingface - transformers documentation: https://huggingface.co/docs/trl/en/clis --- # TRL Training Skill You are an expert at using the TRL (Transformers Reinforcement Learning) library to train and fine-tune large language models. ## Overview TRL provides CLI commands for post-training foundation models using state-of-the-art techniques: - **SFT** (Supervised Fine-Tuning): Fine-tune models on instruction-following or conversational datasets - **DPO** (Direct Preference Optimization): Align models using preference data - **GRPO** (Group Relative Policy Optimization): Train models by ranking multiple sampled outputs relative to each other and optimizing based on their comparative rewards. - **RLOO** (Reinforce Leave One Out): Online RL training with generation-based rewards - **Reward Model Training**: Train reward models for RLHF TRL is built on top of Hugging Face Transformers and Accelerate, providing seamless integration with the Hugging Face ecosystem. ## Core Commands ### trl sft - Supervised Fine-Tuning Fine-tune language models on instruction-following or conversational datasets. **Full training:** ```bash trl sft \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2.0e-5 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --eos_token '<|im_end|>' \ --eval_strategy steps \ --eval_steps 100 \ --output_dir Qwen2-0.5B-SFT \ --push_to_hub ``` **Train with LoRA adapters:** ```bash trl sft \ --model_name_or_path Qwen/Qwen2-0.5B \ --dataset_name trl-lib/Capybara \ --learning_rate 2.0e-4 \ --num_train_epochs 1 \ --packing \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --eos_token '<|im_end|>' \ --eval_strategy steps \ --eval_steps 100 \ --use_peft \ --lora_r 32 \ --lora_alpha 16 \ --output_dir Qwen2-0.5B-SFT \ --push_to_hub ``` ### trl dpo - Direct Preference Optimization Align models using preference data (chosen/rejected pairs). **Full training:** ```bash trl dpo \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --learning_rate 5.0e-7 \ --num_train_epochs 1 \ --per_device_train_batch_size 2 \ --max_steps 1000 \ --gradient_accumulation_steps 8 \ --eval_strategy steps \ --eval_steps 50 \ --output_dir Qwen2-0.5B-DPO \ --no_remove_unused_columns ``` **Train with LoRA adapters:** ```bash trl dpo \ --dataset_name trl-lib/ultrafeedback_binarized \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --learning_rate 5.0e-6 \ --num_train_epochs 1 \ --per_device_train_batch_size 2 \ --max_steps 1000 \ --gradient_accumulation_steps 8 \ --eval_strategy steps \ --eval_steps 50 \ --output_dir Qwen2-0.5B-DPO \ --no_remove_unused_columns \ --use_peft \ --lora_r 32 \ --lora_alpha 16 ``` ### trl grpo - Group Relative Policy Optimization Train models using reward functions or LLM-as-a-judge for evaluating generations and providing rewards. **Basic usage:** ```bash trl grpo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/gsm8k \ --reward_funcs accuracy_reward \ --output_dir Qwen2-0.5B-GRPO \ --push_to_hub ``` ### trl rloo - Reinforce Leave One Out Online RL training where the model generates text and receives rewards based on custom criteria. **Basic usage:** ```bash trl rloo \ --model_name_or_path Qwen/Qwen2.5-0.5B \ --dataset_name trl-lib/tldr \ --reward_model_name_or_path sentiment-analysis:nlptown/bert-base-multilingual-uncased-sentiment \ --output_dir Qwen2-0.5B-RLOO \ --push_to_hub ``` ### trl reward - Reward Model Training Train a reward model to score text quality for RLHF. **Full training:** ```bash trl reward \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --output_dir Qwen2-0.5B-Reward \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-5 \ --eval_strategy steps \ --eval_steps 50 \ --max_length 2048 ``` **Train with LoRA adapters:** ```bash trl reward \ --model_name_or_path Qwen/Qwen2-0.5B-Instruct \ --dataset_name trl-lib/ultrafeedback_binarized \ --output_dir Qwen2-0.5B-Reward-LoRA \ --per_device_train_batch_size 8 \ --num_train_epochs 1 \ --learning_rate 1.0e-4 \ --eval_strategy steps \ --eval_steps 50 \ --max_length 2048 \ --use_peft \ --lora_task_type SEQ_CLS \ --lora_r 32 \ --lora_alpha 16 ``` ## Configuration Files TRL supports YAML configuration files for reproducible training. All CLI arguments can be specified in a config file. **Example config (sft_config.yaml):** ```yaml model_name_or_path: Qwen/Qwen2.5-0.5B dataset_name: trl-lib/Capybara learning_rate: 2.0e-5 num_train_epochs: 1 per_device_train_batch_size: 8 gradient_accumulation_steps: 2 output_dir: ./sft_output use_peft: true lora_r: 16 lora_alpha: 16 report_to: trackio ``` **Launch with config:** ```bash trl sft --config sft_config.yaml ``` **Override config values:** ```bash trl sft --config sft_config.yaml --learning_rate 1.0e-5 ``` ## Distributed Training TRL integrates with Accelerate for multi-GPU and multi-node training. **Multi-GPU training:** ```bash trl sft \ --config sft_config.yaml \ --num_processes 4 ``` **Use predefined Accelerate configs:** TRL provides predefined configs: `single_gpu`, `multi_gpu`, `fsdp1`, `fsdp2`, `zero1`, `zero2`, `zero3` ```bash trl sft \ --config sft_config.yaml \ --accelerate_config zero2 ``` **Custom Accelerate config:** ```bash # Generate custom config accelerate config # Use custom config trl sft --config sft_config.yaml --config_file ~/.cache/huggingface/accelerate/default_config.yaml ``` **Fully Sharded Data Parallel (FSDP):** ```bash trl sft --config sft_config.yaml --accelerate_config fsdp2 ``` **DeepSpeed ZeRO:** ```bash trl sft --config sft_config.yaml --accelerate_config zero3 ``` ## Troubleshooting ### CUDA Out of Memory - Reduce `--per_device_train_batch_size` and increase `--gradient_accumulation_steps` - Enable `--use_peft` for LoRA training - Use `--gradient_checkpointing` to save memory - Try smaller model or longer sequence truncation ### Dataset Loading Issues - Verify dataset exists: check Hugging Face Hub or local path - Check dataset format matches expected columns - Use `--dataset_config` for multi-config datasets - Inspect dataset: `from datasets import load_dataset; ds = load_dataset(name)` ### Model Loading Issues - Verify model exists on Hugging Face Hub - Check if gated model requires authentication: `hf auth login` - For local models, provide absolute path - Ensure sufficient disk space and memory ### Slow Training - Enable dataset `--packing` for short sequences - Use larger `--per_device_train_batch_size` if memory allows - Enable `--tf32` for faster computation on Ampere GPUs - Use `--bf16` on supported hardware - Consider multi-GPU training with `--num_processes` ### Generation Issues (GRPO/RLOO) - Check prompt format in dataset - Adjust `--temperature` and `--top_p` for generation - Verify the reward function (for GRPO/RLOO) ## Additional Resources - **Documentation**: https://huggingface.co/docs/trl - **GitHub**: https://github.com/huggingface/trl - **Examples**: https://github.com/huggingface/trl/tree/main/examples ## Best Practices 1. **Start with SFT**: Always fine-tune base models with SFT before preference alignment 2. **Use LoRA for efficiency**: Enable `--use_peft` for faster training and lower memory 3. **Monitor training**: Use `--report_to trackio` (or `--report_to wandb` or `--report_to tensorboard`) for tracking 4. **Save checkpoints**: TRL automatically saves checkpoints in `--output_dir` 5. **Test on small datasets first**: Verify pipeline works before full training 6. **Use configuration files**: Create YAML configs for reproducibility 7. **Leverage Accelerate**: Use multi-GPU training for faster iteration When helping users with TRL: - Always check which training method is appropriate for their use case - Verify dataset format matches the expected schema - Recommend starting with smaller models for testing - Suggest LoRA for resource-constrained environments - Point to specific documentation sections for advanced features ================================================ FILE: trl/templates/completions_dataset_card.md ================================================ --- {{ card_data }} --- # TRL Completion logs This dataset contains the completions generated during training using `trl`. {% if hub_model_id %} Find the trained model at https://huggingface.co/{{ hub_model_id }}. {% endif %} The completions are stored in parquet files, and each file contains the completions for a single step of training (depending on the `logging_steps` argument). Each file contains the following columns: - `step`: the step of training - `prompt`: the prompt used to generate the completion - `completion`: the completion generated by the model - ``: the reward(s) assigned to the completion by the reward function(s) used during training - `advantage`: the computed advantage for the completion Having this data stored as a simple parquet file makes it easy to load and analyze using the Datasets Viewer, Polars, Pandas, etc. You can load the dataset using the `datasets` library: ```python import datasets dataset = datasets.load_dataset("{{ repo_id }}") ``` You can also load the dataset using Polars: ```python import polars as pl # Login using e.g. `huggingface-cli login` to access this dataset if it's private df = pl.read_parquet(f"hf://datasets/{{ repo_id }}/*.parquet") ``` ================================================ FILE: trl/templates/lm_model_card.md ================================================ --- {{ card_data }} --- # Model Card for {{ model_name }} This model is a fine-tuned version of [{{ base_model }}](https://huggingface.co/{{ base_model }}){% if dataset_name %} on the [{{ dataset_name }}](https://huggingface.co/datasets/{{ dataset_name }}) dataset{% endif %}. It has been trained using [TRL](https://github.com/huggingface/trl). ## Quick start ```python from transformers import pipeline question = "If you had a time machine, but could only go to the past or the future once and never return, which would you choose and why?" generator = pipeline("text-generation", model="{{ hub_model_id }}", device="cuda") output = generator([{"role": "user", "content": question}], max_new_tokens=128, return_full_text=False)[0] print(output["generated_text"]) ``` ## Training procedure {% if wandb_url %}[Visualize in Weights & Biases]({{ wandb_url }}){% endif %} {% if trackio_url %}[Visualize in Trackio]({{ trackio_url }}){% endif %} {% if comet_url %}[Visualize in Comet]({{ comet_url }}){% endif %} This model was trained with {{ trainer_name }}{% if paper_id %}, a method introduced in [{{ paper_title }}](https://huggingface.co/papers/{{ paper_id }}){% endif %}. ### Framework versions - TRL: {{ trl_version }} - Transformers: {{ transformers_version }} - Pytorch: {{ pytorch_version }} - Datasets: {{ datasets_version }} - Tokenizers: {{ tokenizers_version }} ## Citations {% if trainer_citation %}Cite {{ trainer_name }} as: ```bibtex {{ trainer_citation }} ```{% endif %} Cite TRL as: ```bibtex {% raw %}@software{vonwerra2020trl, title = {{TRL: Transformers Reinforcement Learning}}, author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and Beeching, Edward and Thrush, Tristan and Lambert, Nathan and Huang, Shengyi and Rasul, Kashif and Gallouédec, Quentin}, license = {Apache-2.0}, url = {https://github.com/huggingface/trl}, year = {2020} }{% endraw %} ``` ================================================ FILE: trl/templates/rm_model_card.md ================================================ --- {{ card_data }} --- # Model Card for {{ model_name }} This model is a fine-tuned version of [{{ base_model }}](https://huggingface.co/{{ base_model }}){% if dataset_name %} on the [{{ dataset_name }}](https://huggingface.co/datasets/{{ dataset_name }}) dataset{% endif %}. It has been trained using [TRL](https://github.com/huggingface/trl). ## Quick start ```python from transformers import pipeline text = "The capital of France is Paris." rewarder = pipeline(model="{{ hub_model_id }}", device="cuda") output = rewarder(text)[0] print(output["score"]) ``` ## Training procedure {% if wandb_url %}[Visualize in Weights & Biases]({{ wandb_url }}){% endif %} {% if trackio_url %}[Visualize in Trackio]({{ trackio_url }}){% endif %} {% if comet_url %}[Visualize in Comet]({{ comet_url }}){% endif %} This model was trained with {{ trainer_name }}{% if paper_id %}, a method introduced in [{{ paper_title }}](https://huggingface.co/papers/{{ paper_id }}){% endif %}. ### Framework versions - TRL: {{ trl_version }} - Transformers: {{ transformers_version }} - Pytorch: {{ pytorch_version }} - Datasets: {{ datasets_version }} - Tokenizers: {{ tokenizers_version }} ## Citations {% if trainer_citation %}Cite {{ trainer_name }} as: ```bibtex {{ trainer_citation }} ```{% endif %} Cite TRL as: ```bibtex {% raw %}@software{vonwerra2020trl, title = {{TRL: Transformers Reinforcement Learning}}, author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and Beeching, Edward and Thrush, Tristan and Lambert, Nathan and Huang, Shengyi and Rasul, Kashif and Gallouédec, Quentin}, license = {Apache-2.0}, url = {https://github.com/huggingface/trl}, year = {2020} }{% endraw %} ``` ================================================ FILE: trl/trainer/__init__.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import TYPE_CHECKING from .._lazy_module import _LazyModule _import_structure = { "callbacks": [ "BEMACallback", "LogCompletionsCallback", "RichProgressCallback", "SyncRefModelCallback", "WeaveCallback", ], "dpo_config": ["DPOConfig"], "dpo_trainer": ["DPOTrainer"], "grpo_config": ["GRPOConfig"], "grpo_trainer": ["GRPOTrainer"], "kto_config": ["KTOConfig"], "kto_trainer": ["KTOTrainer"], "model_config": ["ModelConfig"], "reward_config": ["RewardConfig"], "reward_trainer": ["RewardTrainer"], "rloo_config": ["RLOOConfig"], "rloo_trainer": ["RLOOTrainer"], "sft_config": ["SFTConfig"], "sft_trainer": ["SFTTrainer"], "utils": [ "disable_dropout_in_model", "ensure_master_addr_port", "get_kbit_device_map", "get_peft_config", "get_quantization_config", ], } if TYPE_CHECKING: from .callbacks import ( BEMACallback, LogCompletionsCallback, RichProgressCallback, SyncRefModelCallback, WeaveCallback, ) from .dpo_config import DPOConfig from .dpo_trainer import DPOTrainer from .grpo_config import GRPOConfig from .grpo_trainer import GRPOTrainer from .kto_config import KTOConfig from .kto_trainer import KTOTrainer from .model_config import ModelConfig from .reward_config import RewardConfig from .reward_trainer import RewardTrainer from .rloo_config import RLOOConfig from .rloo_trainer import RLOOTrainer from .sft_config import SFTConfig from .sft_trainer import SFTTrainer from .utils import ( disable_dropout_in_model, ensure_master_addr_port, get_kbit_device_map, get_peft_config, get_quantization_config, ) else: import sys sys.modules[__name__] = _LazyModule(__name__, globals()["__file__"], _import_structure, module_spec=__spec__) ================================================ FILE: trl/trainer/base_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from transformers import TrainingArguments @dataclass class _BaseConfig(TrainingArguments): """ Base configuration class for all TRL trainer configurations. Subclasses [`~transformers.TrainingArguments`] and overrides fields that are common across TRL trainers or that contain unescaped "%" characters which would cause argparse to raise a `TypeError` when rendering `--help` output. Parameters: logging_steps (`int` or `float`, *optional*, defaults to `10`): Number of update steps between two logs if `logging_strategy="steps"`. Should be an integer or a float in range `[0,1)`. If smaller than 1, will be interpreted as ratio of total training steps. gradient_checkpointing (`bool`, *optional*, defaults to `True`): Whether to enable gradient checkpointing to trade compute for memory. Reduces memory usage by clearing activations during forward pass and recomputing them during backward pass. Enables training larger models or batch sizes at the cost of ~20% slower training. bf16 (`bool`, *optional*): Whether to use bfloat16 (BF16) mixed precision instead of 32-bit. Generally preferred over FP16 due to better numerical stability and no loss scaling required. Requires Ampere or higher NVIDIA architecture or Intel XPU or using CPU (use_cpu) or Ascend NPU. If not set, it defaults to `True` if `fp16` is not set. lr_scheduler_kwargs (`dict` or `str`, *optional*): Additional parameters for the lr_scheduler, such as `{'num_cycles': 1}` for cosine with hard restarts. See the documentation of each scheduler for possible values. use_liger_kernel (`bool`, *optional*, defaults to `False`): Enable [Liger Kernel](https://github.com/linkedin/Liger-Kernel) optimizations. Increases multi-GPU throughput by ~20% and reduces memory usage by ~60%. Works with Flash Attention, FSDP, and DeepSpeed. Currently, supports Llama, Mistral, Mixtral, and Gemma models. torch_empty_cache_steps (`int`, *optional*): Number of steps to wait before calling `torch..empty_cache()`. If left unset or set to None, cache will not be emptied. This can help avoid CUDA out-of-memory errors by lowering peak VRAM usage at a cost of about [10% slower performance](https://github.com/huggingface/transformers/issues/31372). """ # Override fields from TrainingArguments to set defaults preferred by all TRL trainers. logging_steps: float = field( default=10, metadata={ "help": "Log every X updates steps. Should be an integer or a float in range `[0,1)`. If smaller than 1, " "will be interpreted as ratio of total training steps." }, ) gradient_checkpointing: bool = field( default=True, metadata={ "help": "Enable gradient checkpointing to trade compute for memory. Reduces memory at the cost of ~20%% slower training." }, ) bf16: bool | None = field( default=None, metadata={ "help": "Whether to use bf16 (mixed) precision instead of 32-bit. Requires Ampere or higher NVIDIA " "architecture or Intel XPU or using CPU (use_cpu) or Ascend NPU. If not set, it defaults to `True` if " "`fp16` is not set." }, ) # Transformers 4.57.0 introduced a bug that caused the dtype of `lr_scheduler_kwargs` to be unparsable. This issue # was fixed in https://github.com/huggingface/transformers/pull/41322 and released in 4.57.5. We add a temporary # workaround here, which can be removed once we drop support for versions older than 4.57.5. lr_scheduler_kwargs: dict | str | None = field( default=None, metadata={ "help": "Additional parameters for the lr_scheduler, such as {'num_cycles': 1} for cosine with hard " "restarts. See the documentation of each scheduler for possible values." }, ) # Override fields from TrainingArguments whose help strings contain unescaped "%" characters. # argparse interprets "%" as a format specifier, raising TypeError when rendering --help output. # Fixed upstream in transformers v5.3.0, but overridden here to support older versions. # - Introduced in v5.2.0; fixed in v5.3.0 use_liger_kernel: bool = field( default=False, metadata={ "help": "Enable Liger Kernel optimizations. Increases throughput by ~20%% and reduces memory by ~60%%." }, ) # - Introduced in v4.54.1; fixed in v5.3.0 torch_empty_cache_steps: int | None = field( default=None, metadata={ "help": "Number of steps to wait before calling `torch..empty_cache()`. Helps avoid CUDA OOM at a cost of ~10%% slower performance. If None, cache will not be emptied." }, ) def __post_init__(self): self.bf16 = not (self.fp16) if self.bf16 is None else self.bf16 super().__post_init__() ================================================ FILE: trl/trainer/base_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from transformers import Trainer, is_wandb_available from .utils import generate_model_card, get_comet_experiment_url, get_config_model_id, get_trackio_space_url if is_wandb_available(): import wandb class _BaseTrainer(Trainer): _tag_names = [] _name = "Base" _paper = {} _template_file = None def create_model_card( self, model_name: str | None = None, dataset_name: str | None = None, tags: str | list[str] | None = None, ): """ Creates a draft of a model card using the information available to the `Trainer`. Args: model_name (`str`, *optional*): Name of the model. dataset_name (`str`, *optional*): Name of the dataset used for training. tags (`str`, `list[str]`, *optional*): Tags to be associated with the model card. """ if not self.is_world_process_zero(): return model_name_or_path = get_config_model_id(self.model.config) if model_name_or_path and not os.path.isdir(model_name_or_path): base_model = model_name_or_path else: base_model = None # Normalize tags if tags is None: tags = set() elif isinstance(tags, str): tags = {tags} else: tags = set(tags) if hasattr(self.model.config, "unsloth_version"): tags.add("unsloth") if "JOB_ID" in os.environ: tags.add("hf_jobs") tags.update(self._tag_names) trackio_url = get_trackio_space_url() # Pop existing Trackio tag and re-add the one with the proper url parameters if trackio_url is not None: for tag in list(tags): if tag.startswith("trackio:"): tags.remove(tag) tags.add(f"trackio:{trackio_url}") tags = list(tags) model_card = generate_model_card( base_model=base_model, model_name=model_name, hub_model_id=self.hub_model_id, dataset_name=dataset_name, tags=tags, wandb_url=wandb.run.url if is_wandb_available() and wandb.run is not None else None, trackio_url=trackio_url, comet_url=get_comet_experiment_url(), trainer_name=self._name, trainer_citation=self._paper.get("citation"), template_file=self._template_file, paper_title=self._paper.get("title"), paper_id=self._paper.get("id"), ) model_card.save(os.path.join(self.args.output_dir, "README.md")) ================================================ FILE: trl/trainer/callbacks.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import pandas as pd import torch from accelerate import Accelerator from accelerate.state import AcceleratorState from accelerate.utils import gather_object, is_wandb_available from transformers import ( GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, Trainer, TrainerCallback, TrainerControl, TrainerState, TrainingArguments, ) from transformers.trainer_utils import has_length from transformers.utils import is_rich_available from ..data_utils import maybe_apply_chat_template from ..import_utils import is_weave_available from ..models.utils import unwrap_model_for_generation from .utils import log_table_to_comet_experiment if is_rich_available(): from rich.columns import Columns from rich.console import Console, Group from rich.live import Live from rich.panel import Panel from rich.progress import Progress from rich.table import Table if is_wandb_available(): import wandb if is_weave_available(): import weave from weave import EvaluationLogger from weave.trace.context import weave_client_context # Logger for module-level logging logger = logging.getLogger(__name__) def _generate_completions( prompts: list[str], model: PreTrainedModel, tokenizer: PreTrainedTokenizerBase, accelerator: Accelerator, generation_config: GenerationConfig | None, batch_size: int = 1, ) -> list[str]: """ Generates completions for a list of pre-formatted prompts from the given model. Args: prompts (list[str]): A list of input prompts for which completions are to be generated. model (PreTrainedModel): The pre-trained model to be used for generation. tokenizer (PreTrainedTokenizerBase): The tokenizer to be used for encoding and decoding. accelerator (Accelerator): The accelerator to be used for model execution. generation_config (GenerationConfig): Configuration for text generation. batch_size (int, optional): The number of prompts to process in each batch. Default is 1. Returns: list[str]: A list of generated text completions corresponding to the input prompts. """ completions = [] # TODO: Override model.generation_config with generation_kwargs with unwrap_model_for_generation(model, accelerator) as unwrapped_model: for idx in range(0, len(prompts), batch_size): batch = prompts[idx : idx + batch_size] tokenized_batch = tokenizer(batch, return_tensors="pt", padding=True, truncation=True).to(model.device) generations = unwrapped_model.generate( **tokenized_batch, generation_config=generation_config, ) for prompt, generation in zip(tokenized_batch.input_ids, generations, strict=True): # Remove prompt from generation generation = generation[len(prompt) :] completion = tokenizer.decode(generation, skip_special_tokens=True) completions.append(completion) return completions class SyncRefModelCallback(TrainerCallback): """ Callback to synchronize the model with a reference model. """ def __init__( self, ref_model: PreTrainedModel | torch.nn.Module, accelerator: Accelerator | None, ): self.accelerator = accelerator self.ref_model = ref_model @staticmethod def _sync_target_model(model, target_model, alpha): for target_param, copy_param in zip(target_model.parameters(), model.parameters(), strict=True): target_param.data.mul_(1.0 - alpha).add_(copy_param.data, alpha=alpha) @staticmethod def sync_target_model(model, target_model, alpha): deepspeed_plugin = AcceleratorState().deepspeed_plugin if deepspeed_plugin is not None and deepspeed_plugin.zero_stage == 3: import deepspeed with deepspeed.zero.GatheredParameters( list(model.parameters()) + list(target_model.parameters()), modifier_rank=0 ): if deepspeed.comm.get_rank() == 0: SyncRefModelCallback._sync_target_model(model, target_model, alpha) else: SyncRefModelCallback._sync_target_model(model, target_model, alpha) def on_step_end(self, args, state, control, **kwargs): model: PreTrainedModel = kwargs["model"] if self.ref_model is not None and state.global_step % args.ref_model_sync_steps == 0: if self.accelerator: model = self.accelerator.unwrap_model(model) self.sync_target_model(model, self.ref_model, args.ref_model_mixup_alpha) class RichProgressCallback(TrainerCallback): """ A [`TrainerCallback`] that displays the progress of training or evaluation using Rich. """ def __init__(self): if not is_rich_available(): raise ImportError("RichProgressCallback requires the `rich` extra. To install, run `pip install rich`.") self.training_bar = None self.evaluation_bar = None self.training_task = None self.evaluation_task = None self.rich_group = None self.rich_console = None self.training_status = None self.current_step = None def on_train_begin(self, args, state, control, **kwargs): if not state.is_world_process_zero: return self.training_bar = Progress() self.evaluation_bar = Progress() self.rich_console = Console() self.training_status = self.rich_console.status("Nothing to log yet ...") self.rich_group = Live(Panel(Group(self.training_bar, self.evaluation_bar, self.training_status))) self.rich_group.start() self.training_task = self.training_bar.add_task("[blue]Training ", total=state.max_steps) self.current_step = 0 def on_step_end(self, args, state, control, **kwargs): if not state.is_world_process_zero: return self.training_bar.update(self.training_task, advance=state.global_step - self.current_step, update=True) self.current_step = state.global_step def on_prediction_step(self, args, state, control, eval_dataloader=None, **kwargs): if not state.is_world_process_zero: return if has_length(eval_dataloader): if self.evaluation_task is None: self.evaluation_task = self.evaluation_bar.add_task("[blue]Evaluation", total=len(eval_dataloader)) self.evaluation_bar.update(self.evaluation_task, advance=1, update=True) def on_evaluate(self, args, state, control, **kwargs): if not state.is_world_process_zero: return if self.evaluation_task is not None: self.evaluation_bar.remove_task(self.evaluation_task) self.evaluation_task = None def on_predict(self, args, state, control, **kwargs): if not state.is_world_process_zero: return if self.evaluation_task is not None: self.evaluation_bar.remove_task(self.evaluation_task) self.evaluation_task = None def on_log(self, args, state, control, logs=None, **kwargs): if not (state.is_world_process_zero and self.training_bar): return # Group keys by top-level prefix grouped_logs = {} for key, value in logs.items(): parts = key.split("/") group = parts[0] if len(parts) > 1 else None subkey = "/".join(parts[1:]) if len(parts) > 1 else key grouped_logs.setdefault(group, {})[subkey] = value # Create a table per group tables = [] for group_name, metrics in grouped_logs.items(): table = Table( title=f"[bold blue]{group_name}[/]" if group_name else None, header_style="bold magenta", box=None ) table.add_column("Metric", justify="left", no_wrap=True) table.add_column("Value", justify="right") for metric, val in metrics.items(): formatted = f"{val:.3f}" if isinstance(val, (float, int)) else str(val) table.add_row(metric, formatted) tables.append(Panel(table, border_style="cyan", padding=(0, 1))) # Arrange tables in columns using Columns column_layout = Columns(tables, equal=False, expand=True) self.training_status.update( Panel(column_layout, title=f"[bold green]Step {state.global_step}[/bold green]", border_style="green") ) def on_train_end(self, args, state, control, **kwargs): if not state.is_world_process_zero: return self.rich_group.stop() self.training_bar = None self.evaluation_bar = None self.training_task = None self.evaluation_task = None self.rich_group = None self.rich_console = None self.training_status = None self.current_step = None class LogCompletionsCallback(TrainerCallback): r""" A [`~transformers.TrainerCallback`] that logs completions to Weights & Biases and/or Comet. Usage: ```python trainer = DPOTrainer(...) completions_callback = LogCompletionsCallback(trainer=trainer) trainer.add_callback(completions_callback) ``` Args: trainer (`Trainer`): Trainer to which the callback will be attached. The trainer's evaluation dataset must include a `"prompt"` column containing the prompts for generating completions. generation_config ([`~transformers.GenerationConfig`], *optional*): The generation config to use for generating completions. num_prompts (`int`, *optional*): The number of prompts to generate completions for. If not provided, defaults to the number of examples in the evaluation dataset. freq (`int`, *optional*): The frequency at which to log completions. If not provided, defaults to the trainer's `eval_steps`. """ def __init__( self, trainer: Trainer, generation_config: GenerationConfig | None = None, num_prompts: int | None = None, freq: int | None = None, ): self.trainer = trainer self.generation_config = generation_config self.freq = freq self.table = [] self._last_logged_step = -1 if self.trainer.eval_dataset is None: raise ValueError("Trainer must have an evaluation dataset to use the LogCompletionsCallback.") else: self.eval_dataset = self.trainer.eval_dataset if num_prompts is not None: self.eval_dataset = self.eval_dataset.select(range(num_prompts)) def on_step_end(self, args, state, control, **kwargs): # Only log once per step (this method may be called multiple times) if state.global_step == self._last_logged_step: return # Only log every `freq` steps (if no `freq` is provided, log every `eval_steps` steps) freq = self.freq or state.eval_steps if state.global_step % freq != 0: return tokenizer = kwargs["processing_class"] tokenizer.padding_side = "left" accelerator = self.trainer.accelerator model = self.trainer.model_wrapped with accelerator.split_between_processes(self.eval_dataset["prompt"]) as prompts: prompts = [maybe_apply_chat_template({"prompt": prompt}, tokenizer)["prompt"] for prompt in prompts] completions = _generate_completions( prompts, model=model, tokenizer=tokenizer, accelerator=accelerator, generation_config=self.generation_config, batch_size=args.per_device_eval_batch_size, ) completions = gather_object(completions) prompts = gather_object(prompts) # Build the data to log if self.trainer.accelerator.is_main_process: global_step = [str(state.global_step)] * len(prompts) data = list(zip(global_step, prompts, completions, strict=True)) self.table.extend(data) table = pd.DataFrame(columns=["step", "prompt", "completion"], data=self.table) if "wandb" in args.report_to: wandb.log({"completions": table}) if "comet_ml" in args.report_to: log_table_to_comet_experiment( name="completions.csv", table=table, ) # Save the last logged step, so we don't log the same completions multiple times self._last_logged_step = state.global_step class WeaveCallback(TrainerCallback): r""" A [`~transformers.TrainerCallback`] that logs traces and evaluations to W&B Weave. The callback uses https://weave-docs.wandb.ai/guides/evaluation/evaluation_logger/ to log traces and evaluations at each evaluation step. Supports two modes based on the `scorers` parameter: - **Tracing Mode** (when scorers=None): Logs predictions for data exploration and analysis - **Evaluation Mode** (when scorers provided): Logs predictions with scoring and summary metrics Both modes use Weave's EvaluationLogger for structured, consistent data logging. The callback logs data during evaluation phases (`on_evaluate`) rather than training steps, making it more efficient and semantically correct. It gracefully handles missing weave installation by logging warnings and skipping weave-specific functionality. It also checks for existing weave clients before initializing new ones. Usage: ```python # Tracing mode (just log predictions) trainer = DPOTrainer(...) weave_callback = WeaveTraceCallback(trainer=trainer) # project_name optional trainer.add_callback(weave_callback) # Or specify a project name weave_callback = WeaveTraceCallback(trainer=trainer, project_name="my-llm-training") trainer.add_callback(weave_callback) # Evaluation mode (log predictions + scores + summary) def accuracy_scorer(prompt: str, completion: str) -> float: # Your scoring logic here (metadata available via eval_attributes) return score weave_callback = WeaveTraceCallback( trainer=trainer, project_name="my-llm-training", # optional and needed only if weave client is not initialized scorers={"accuracy": accuracy_scorer}, ) trainer.add_callback(weave_callback) ``` Args: trainer (`Trainer`): Trainer to which the callback will be attached. The trainer's evaluation dataset must include a `"prompt"` column containing the prompts for generating completions. project_name (`str`, *optional*): Name of the Weave project where data will be logged. If not provided, will try to use existing weave client or fall back to the active wandb run's project name. Raises an error if none of these are available. scorers (`dict[str, Callable]`, *optional*): Dictionary mapping scorer names to scorer functions. If `None`, operates in tracing mode (predictions only). If provided, operates in evaluation mode (predictions + scores + summary). Scorer functions should have signature: `scorer(prompt: str, completion: str) -> float | int` generation_config ([`~transformers.GenerationConfig`], *optional*): Generation config to use for generating completions. num_prompts (`int` or `None`, *optional*): Number of prompts to generate completions for. If not provided, defaults to the number of examples in the evaluation dataset. dataset_name (`str`, *optional*, defaults to `"eval_dataset"`): Name for the dataset metadata in Weave. model_name (`str`, *optional*): Name for the model metadata in Weave. If not provided, attempts to extract from model config. """ def __init__( self, trainer: Trainer, project_name: str | None = None, scorers: dict[str, callable] | None = None, generation_config: GenerationConfig | None = None, num_prompts: int | None = None, dataset_name: str = "eval_dataset", model_name: str | None = None, ): self.trainer = trainer self.project_name = project_name self.scorers = scorers or {} self.generation_config = generation_config self.dataset_name = dataset_name self.model_name = model_name self._last_logged_step = -1 self._weave_initialized = False self._eval_logger = None if self.trainer.eval_dataset is None: raise ValueError("Trainer must have an evaluation dataset to use the WeaveCallback.") else: self.eval_dataset = self.trainer.eval_dataset if num_prompts is not None: self.eval_dataset = self.eval_dataset.select(range(num_prompts)) def _initialize_weave(self): """Initialize Weave and EvaluationLogger if not already initialized.""" if not self._weave_initialized: if not is_weave_available(): logger.warning("Weave is not available. Please install weave to enable logging: `pip install weave`") return if wc := weave_client_context.get_weave_client(): self._weave_client = wc else: if self.project_name is None: if is_wandb_available(): if wandb.run is not None: self.project_name = wandb.run.entity + "/" + wandb.run.project logger.info(f"Using project name from active wandb run: {self.project_name}") if self.project_name is None: raise ValueError( "No existing Weave client found and no project_name provided. " "Please either initialize weave with `weave.init('project-name')`, " "provide a project_name to the `WeaveTraceCallback`, " "or ensure an active wandb run exists." ) self._weave_client = weave.init(self.project_name) logger.info(f"Initialized Weave with project: {self.project_name}") if self.model_name is None: self.model_name = getattr(self.trainer.model_wrapped.config, "_name_or_path", "unknown_model") self._EvaluationLogger = EvaluationLogger self._weave_initialized = True @property def is_evaluation_mode(self) -> bool: """True if scorers are provided (evaluation mode), False for tracing mode.""" return bool(self.scorers) def on_train_begin(self, args, state, control, **kwargs): """Initialize Weave when training begins.""" self._initialize_weave() def on_evaluate(self, args, state, control, **kwargs): if state.global_step == self._last_logged_step: return self._initialize_weave() if not self._weave_initialized: logger.debug("Weave not initialized, skipping logging") return tokenizer = kwargs["processing_class"] tokenizer.padding_side = "left" accelerator = self.trainer.accelerator model = self.trainer.model_wrapped with accelerator.split_between_processes(self.eval_dataset["prompt"]) as prompts: prompts = [maybe_apply_chat_template({"prompt": prompt}, tokenizer)["prompt"] for prompt in prompts] completions = _generate_completions( prompts=prompts, model=model, tokenizer=tokenizer, accelerator=accelerator, generation_config=self.generation_config, batch_size=args.per_device_eval_batch_size, ) all_prompts = gather_object(prompts) all_completions = gather_object(completions) if self.trainer.accelerator.is_main_process: eval_attributes = { "training_step": state.global_step, "model_name": self.model_name, "generation_config": (self.generation_config.to_dict() if self.generation_config else None), } eval_logger = self._EvaluationLogger( model=self.model_name, dataset=self.dataset_name, eval_attributes=eval_attributes, ) successful_predictions = 0 total_score_values = {} # For summary statistics for prompt, completion in zip(all_prompts, all_completions, strict=True): try: pred_logger = eval_logger.log_prediction(inputs={"prompt": prompt}, output=completion) if self.is_evaluation_mode: for scorer_name, scorer_func in self.scorers.items(): try: score = scorer_func(prompt, completion) pred_logger.log_score(scorer=scorer_name, score=score) if scorer_name not in total_score_values: total_score_values[scorer_name] = [] total_score_values[scorer_name].append(score) except Exception as scorer_e: logger.warning(f"Failed to apply scorer '{scorer_name}': {scorer_e}") pred_logger.finish() successful_predictions += 1 except Exception as pred_e: logger.warning(f"Failed to log prediction for prompt: {pred_e}") # Continue with other predictions even if one fails if self.is_evaluation_mode and total_score_values: try: summary_stats = { "total_predictions": len(all_prompts), "successful_predictions": successful_predictions, } for scorer_name, scores in total_score_values.items(): if scores: # Only if we have valid scores summary_stats[f"avg_{scorer_name}"] = sum(scores) / len(scores) eval_logger.log_summary(summary_stats) except Exception as summary_e: logger.warning(f"Failed to log summary: {summary_e}") else: try: eval_logger.finish() except Exception as finish_e: logger.warning(f"Failed to finish evaluation logger: {finish_e}") self._last_logged_step = state.global_step class BEMACallback(TrainerCallback): # docstyle-ignore r""" A [`~transformers.TrainerCallback`] that implements [BEMA](https://huggingface.co/papers/2508.00180) (Bias-Corrected Exponential Moving Average) by [Adam Block](https://huggingface.co/abblock) and [Cyril Zhang](https://huggingface.co/cyrilzhang). Code from https://github.com/abblock/bema under MIT license. BEMA computes model weights that scale like: $$ \theta_t' = \alpha_t \cdot (\theta_t - \theta_0) + \text{EMA}_t $$ where \\( \theta_t \\) is the current model weights, \\( \theta_0 \\) is a snapshot of the model weights at the first `update_after` step, \\( \text{EMA}_t \\) is the exponential moving average of the model weights, and \\( \alpha_t \\) is a scaling factor that decays with the number of steps \\( t \\) as $$ \alpha_t = (\rho + \gamma \cdot t)^{-\eta}. $$ The EMA is computed as: $$ \text{EMA}_t = (1 - \beta_t) \cdot \text{EMA}_{t-1} + \beta_t \cdot \theta_t $$ where \\( \beta_t \\) is a decay factor that decays with the number of steps \\( t \\) as $$ \beta_t = (\rho + \gamma \cdot t)^{-\kappa}. $$ Args: update_freq (`int`, *optional*, defaults to `400`): Update the BEMA weights every X steps. Denoted this as \\( \phi \\) in the paper. ema_power (`float`, *optional*, defaults to `0.5`): Power for the EMA decay factor. Denoted \\( \kappa \\) in the paper. To disable EMA, set this to `0.0`. bias_power (`float`, *optional*, defaults to `0.2`): Power for the BEMA scaling factor. Denoted \\( \eta \\) in the paper. To disable BEMA, set this to `0.0`. lag (`int`, *optional*, defaults to `10`): Initial offset in the weight decay schedule that controls early-stage smoothness by acting as a virtual starting age for the updates. Denoted as \\( \rho \\) in the paper. update_after (`int`, *optional*, defaults to `0`): Burn-in time before starting to update the BEMA weights. Denoted \\( \tau \\) in the paper. multiplier (`float`, *optional*, defaults to `1.0`): Initial value for the EMA decay factor. Denoted as \\( \gamma \\) in the paper. min_ema_multiplier (`float`, *optional*, defaults to `0.0`): Minimum value for the EMA decay factor. device (`str`, *optional*, defaults to `"cpu"`): Device to use for the BEMA buffers, e.g. `"cpu"` or `"cuda"`. Note that in most cases, this device SHOULD BE DIFFERENT from the device used for training in order to avoid OOM. Example: ```python from trl import BEMACallback trainer = Trainer(..., callbacks=[BEMACallback()]) ``` """ def __init__( self, update_freq: int = 400, ema_power: float = 0.5, bias_power: float = 0.2, lag: int = 10, update_after: int = 0, multiplier: float = 1.0, min_ema_multiplier: float = 0.0, device: str = "cpu", ): # User-provided hyperparams self.update_freq = update_freq self.ema_power = ema_power self.bias_power = bias_power self.lag = lag self.update_after = update_after self.multiplier = multiplier self.min_ema_multiplier = min_ema_multiplier self.device = device # Internal state self.param_names = [] # references to training model param names self.thetat_params = [] # references to training model params self.theta0_params = [] # θ₀ buffers (on self.device) self.ema_params = [] # EMA buffers (on self.device) self.running_model = None # a copy of the model to run BEMA on @staticmethod def _unwrap_model(model): """ Helper function to unwrap model from various wrappers including DataParallel, DistributedDataParallel, DeepSpeed, and FSDP. """ # Handle DeepSpeed if hasattr(model, "module") and hasattr(model, "engine"): # DeepSpeed engine return model.module # Handle FSDP if hasattr(model, "_fsdp_wrapped_module"): # FSDP wrapped model return model._fsdp_wrapped_module # Handle DataParallel/DistributedDataParallel if hasattr(model, "module"): return model.module return model @torch.no_grad() def on_train_begin( self, args: TrainingArguments, state: TrainerState, control: TrainerControl, model: PreTrainedModel, **kwargs ): model = self._unwrap_model(model) # Create a new instance and load state_dict self.running_model = type(model)(model.config).to(self.device) self.running_model.load_state_dict(model.state_dict()) # Cache trainable parameters once in a fixed order for name, param in model.named_parameters(): if not param.requires_grad: continue self.param_names.append(name) self.thetat_params.append(param) # Clone θ₀ and EMA on the same device as model theta0 = param.detach().clone().to(self.device) self.theta0_params.append(theta0) self.ema_params.append(theta0.clone()) # initialize EMA with θ₀ def _ema_beta(self, step: int) -> float: """Compute the EMA decay factor βₜ = (ρ + γ·t)⁻ᵏᵃᵖᵖᵃ.""" beta = (self.lag + self.multiplier * step) ** (-self.ema_power) return max(beta, self.min_ema_multiplier) def _bema_alpha(self, step: int) -> float: """Compute the BEMA scaling factor αₜ = (ρ + γ·t)⁻ᵉᵗᵃ.""" return (self.lag + self.multiplier * step) ** (-self.bias_power) def _update_bema_weights(self, step: int): beta = self._ema_beta(step) alpha = self._bema_alpha(step) # Compute EMA + BEMA in-place and write directly to running_model for thetat, theta0, ema, run_param in zip( self.thetat_params, self.theta0_params, self.ema_params, self.running_model.parameters(), strict=True ): thetat = thetat.detach().to(self.device) ema.mul_(1 - beta).add_(thetat, alpha=beta) # EMA update: ema = (1 - beta) * ema + beta * θₜ run_param.copy_(ema + alpha * (thetat - theta0)) # BEMA update: run_param = ema + alpha * (θₜ - θ₀) @torch.no_grad() def on_step_end( self, args: TrainingArguments, state: TrainerState, control: TrainerControl, model: PreTrainedModel, **kwargs ): step = state.global_step # If we haven't reached the update_after step, skip the BEMA update if step < self.update_after: return # Snapshot θ₀ and EMA at first update if step == self.update_after: for thetat_param, theta0_param, ema_param in zip( self.thetat_params, self.theta0_params, self.ema_params, strict=True ): theta0_param.copy_(thetat_param) ema_param.copy_(thetat_param) # Update BEMA weights every `update_freq` steps elif (step - self.update_after) % self.update_freq == 0: self._update_bema_weights(step) logger.info(f"Updated BEMA weights at step {step}") @torch.no_grad() def on_train_end(self, args: TrainingArguments, state: TrainerState, control: TrainerControl, **kwargs): if state.is_world_process_zero: save_directory = f"{args.output_dir}/bema" self.running_model.save_pretrained(save_directory) logger.info(f"Saved BEMA model to {save_directory}") ================================================ FILE: trl/trainer/dpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from .base_config import _BaseConfig @dataclass class DPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`DPOTrainer`]. This class includes only the parameters that are specific to DPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: > Parameters that control the model model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments for [`~transformers.AutoModelForCausalLM.from_pretrained`], used when the `model` argument of the [`DPOTrainer`] is provided as a string. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model and reference model. > Parameters that control the data preprocessing dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. pad_token (`str`, *optional*): Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that is also `None`, it falls back to `processing_class.eos_token`. max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the tokenized sequence. Sequences longer than `max_length` are truncated from the left or right depending on the `truncation_mode`. If `None`, no truncation is applied. truncation_mode (`str`, *optional*, defaults to `"keep_start"`): Truncation mode to use when the sequence exceeds `max_length`. Possible values are `"keep_end"` and `"keep_start"`. padding_free (`bool`, *optional*, defaults to `False`): Whether to perform forward passes without padding by flattening all sequences in the batch into a single continuous sequence. This reduces memory usage by eliminating padding overhead. Currently, this is only supported with the FlashAttention 2 or 3, which can efficiently handle the flattened batch structure. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. precompute_ref_log_probs (`bool`, *optional*, defaults to `False`): Whether to precompute the reference model log probabilities for the entire training dataset before training. This allows to save memory during training, as the reference model does not need to be kept in memory. precompute_ref_batch_size (`int`, *optional*): Batch size to use when precomputing reference model log probabilities. This can be set higher than the training batch size to speed up preprocessing. If `None`, defaults to `per_device_train_batch_size` for training and `per_device_eval_batch_size` for evaluation. > Parameters that control the training loss_type (`list[str]`, *optional*, defaults to `["sigmoid"]`): Type of loss to use. Possible values are: `'sigmoid'`, `'hinge'`, `'ipo'`, `'exo_pair'`, `'nca_pair'`, `'robust'`, `'bco_pair'`, `'sppo_hard'`, `'aot'`, `'aot_unpaired'`, `'apo_zero'`, `'apo_down'`, `'discopop'`, `'sft'`. If multiple loss types are provided, they will be combined using the weights specified in `loss_weights`. loss_weights (`list[float]`, *optional*): List of loss weights for multi-loss combinations. Used when combining multiple loss types. Example: `[0.8, 0.2, 1.0]` for MPO. If not provided, defaults to equal weights (`1.0`) for all loss types. ld_alpha (`float`, *optional*): α parameter from the LD-DPO paper, which controls the weighting of the verbose token log-probabilities in responses. If `None`, no weighting is applied to the verbose part, and the loss is equivalent to the standard DPO loss. Must be in [0.0, 1.0]: `ld_alpha=1.0` applies no weighting, and `ld_alpha=0.0` masks tokens beyond shared lengths. f_divergence_type (`str`, *optional*, defaults to `"reverse_kl"`): f-divergence regularizer between policy and reference (f-DPO paper). Possible values are: `reverse_kl` (default), `forward_kl`, `js_divergence`, `alpha_divergence`. f_alpha_divergence_coef (`float`, *optional*, defaults to `0.5`): α coefficient for the α-divergence u^-α regularizer, used only when `f_divergence_type='alpha_divergence'`. label_smoothing (`float`, *optional*, defaults to `0.0`): Label smoothing parameter used in Robust DPO and EXO. In Robust DPO, it is interpreted as the probability that a preference label is flipped and must lie in [0.0, 0.5); a typical value recommended by the Robust DPO paper is 0.1. In EXO, it corresponds to the ε label smoothing parameter, for which the paper recommends a typical value of 1e-3. beta (`float`, *optional*, defaults to `0.1`): Parameter controlling the deviation from the reference model. Higher β means less deviation from the reference model. For the IPO loss (`loss_type='ipo'`), this value is the regularization parameter denoted by τ in the [paper](https://huggingface.co/papers/2310.12036). use_weighting (`bool`, *optional*, defaults to `False`): Whether to apply WPO-style weighting (https://huggingface.co/papers/2406.11827) to preference pairs using the policy's length-normalized sequence probabilities. discopop_tau (`float`, *optional*, defaults to `0.05`): τ/temperature parameter from the DiscoPOP paper, which controls the shape of the log-ratio modulated loss when using `loss_type='discopop'`. The paper recommends the default value `discopop_tau=0.05`. activation_offloading (`bool`, *optional*, defaults to `False`): Whether to offload the activations to the CPU. sync_ref_model (`bool`, *optional*, defaults to `False`): Whether to synchronize the reference model with the active model every `ref_model_sync_steps` steps, using the `ref_model_mixup_alpha` parameter. This synchronization originates from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper. `sync_ref_model=True` is not yet compatible with PEFT or `precompute_ref_log_probs=True`. ref_model_mixup_alpha (`float`, *optional*, defaults to `0.6`): α parameter from the TR-DPO paper, which controls the mix between the current policy and the previous reference policy during updates. The reference policy is updated according to the equation: `π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`. ref_model_sync_steps (`int`, *optional*, defaults to `512`): τ parameter from the TR-DPO paper, which determines how frequently the current policy is synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) # Parameters that control the model model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments for `AutoModelForCausalLM.from_pretrained`, used when the `model` argument of " "the `DPOTrainer` is provided as a string." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model and reference model."}, ) # Parameters that control the data preprocessing dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) pad_token: str | None = field( default=None, metadata={ "help": "Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that " "is also `None`, it falls back to `processing_class.eos_token`." }, ) max_length: int | None = field( default=1024, metadata={ "help": "Maximum length of the tokenized sequence. Sequences longer than `max_length` are truncated from " "the left or right depending on the `truncation_mode`. If `None`, no truncation is applied." }, ) truncation_mode: str = field( default="keep_start", metadata={ "help": "Truncation mode to use when the sequence exceeds `max_length`. Possible values are `'keep_end'` " "and `'keep_start'`.", "choices": ["keep_end", "keep_start"], }, ) padding_free: bool = field( default=False, metadata={ "help": "Whether to perform forward passes without padding by flattening all sequences in the batch into " "a single continuous sequence. This reduces memory usage by eliminating padding overhead. Currently, this " "is only supported with the FlashAttention 2 or 3, which can efficiently handle the flattened batch " "structure." }, ) pad_to_multiple_of: int | None = field( default=None, metadata={"help": "If set, the sequences will be padded to a multiple of this value."}, ) precompute_ref_log_probs: bool = field( default=False, metadata={ "help": "Whether to precompute the reference model log probabilities for the entire training dataset " "before training. This allows to save memory during training, as the reference model does not need to be " "kept in memory." }, ) precompute_ref_batch_size: int | None = field( default=None, metadata={ "help": "Batch size to use when precomputing reference model log probabilities. This can be set higher " "than the training batch size to speed up preprocessing. If `None`, defaults to " "`per_device_train_batch_size` for training and `per_device_eval_batch_size` for evaluation." }, ) # Parameters that control the training loss_type: list[str] = field( default_factory=lambda: ["sigmoid"], metadata={ "help": "Type of loss to use. Possible values are: `'sigmoid'`, `'hinge'`, `'ipo'`, `'exo_pair'`, " "`'nca_pair'`, `'robust'`, `'bco_pair'`, `'sppo_hard'`, `'aot'`, `'aot_unpaired'`, `'apo_zero'`, " "`'apo_down'`, `'discopop'`, `'sft'`. If multiple loss types are provided, they will be combined using " "the weights specified in `loss_weights`.", }, ) loss_weights: list[float] | None = field( default=None, metadata={ "help": "List of loss weights for multi-loss combinations. Used when combining multiple loss types. " "Example: `[0.8, 0.2, 1.0]` for MPO. If not provided, defaults to equal weights (`1.0`) for all loss " "types." }, ) ld_alpha: float | None = field( default=None, metadata={ "help": "α parameter from the LD-DPO paper, which controls the weighting of the verbose token " "log-probabilities in responses. If `None`, no weighting is applied to the verbose part, and the loss is " "equivalent to the standard DPO loss. Must be in [0.0, 1.0]: `ld_alpha=1.0` applies no weighting, and " "`ld_alpha=0.0` masks tokens beyond shared lengths.", }, ) f_divergence_type: str = field( default="reverse_kl", metadata={ "help": "f-divergence regularizer between policy and reference (f-DPO paper). Possible values are: " "`reverse_kl` (default), `forward_kl`, `js_divergence`, `alpha_divergence`.", }, ) f_alpha_divergence_coef: float = field( default=0.5, metadata={ "help": "α coefficient for the α-divergence u^-α regularizer, used only when " "`f_divergence_type='alpha_divergence'`." }, ) label_smoothing: float = field( default=0.0, metadata={ "help": "Label smoothing parameter used in Robust DPO and EXO. In Robust DPO, it is interpreted as the " "probability that a preference label is flipped and must lie in [0.0, 0.5); a typical value recommended " "by the Robust DPO paper is 0.1. In EXO, it corresponds to the ε label smoothing parameter, for which the " "paper recommends a typical value of 1e-3." }, ) beta: float = field( default=0.1, metadata={ "help": "Parameter controlling the deviation from the reference model. Higher β means less deviation from " "the reference model. For the IPO loss (`loss_type='ipo'`), this value is the regularization parameter " "denoted by τ in the [paper](https://huggingface.co/papers/2310.12036)." }, ) use_weighting: bool = field( default=False, metadata={ "help": "Whether to apply WPO-style weighting (https://huggingface.co/papers/2406.11827) to preference " "pairs using the policy's length-normalized sequence probabilities." }, ) discopop_tau: float = field( default=0.05, metadata={ "help": "τ/temperature parameter from the DiscoPOP paper, which controls the shape of the log-ratio " "modulated loss when using `loss_type='discopop'`. The paper recommends the default value " "`discopop_tau=0.05`." }, ) activation_offloading: bool = field( default=False, metadata={"help": "Whether to offload the activations to the CPU."}, ) sync_ref_model: bool = field( default=False, metadata={ "help": "Whether to synchronize the reference model with the active model every `ref_model_sync_steps` " "steps, using the `ref_model_mixup_alpha` parameter. This synchronization originates from the " "[TR-DPO](https://huggingface.co/papers/2404.09656) paper. `sync_ref_model=True` is not yet compatible " "with PEFT or `precompute_ref_log_probs=True`." }, ) ref_model_mixup_alpha: float = field( default=0.6, metadata={ "help": "α parameter from the TR-DPO paper, which controls the mix between the current policy and the " "previous reference policy during updates. The reference policy is updated according to the equation: " "`π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`." }, ) ref_model_sync_steps: int = field( default=512, metadata={ "help": "τ parameter from the TR-DPO paper, which determines how frequently the current policy is " "synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`." }, ) def __post_init__(self): if isinstance(self.loss_type, str): self.loss_type = [self.loss_type] if self.loss_weights is not None and len(self.loss_weights) != len(self.loss_type): raise ValueError( "`loss_weights` must have the same length as `loss_type` when combining multiple losses. " f"Got {len(self.loss_weights)} weights for {len(self.loss_type)} loss types." ) super().__post_init__() ================================================ FILE: trl/trainer/dpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import json import os import textwrap from collections import defaultdict from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Any import numpy as np import torch import torch.nn.functional as F import transformers from accelerate import PartialState from accelerate.logging import get_logger from accelerate.utils import is_peft_model, tqdm from datasets import Dataset, IterableDataset, IterableDatasetDict from datasets.fingerprint import Hasher from packaging.version import Version from torch.utils.data import DataLoader from transformers import ( AutoProcessor, DataCollator, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, ) from transformers.data.data_collator import DataCollatorMixin from transformers.trainer_callback import TrainerCallback from transformers.trainer_utils import EvalPrediction from transformers.utils import is_liger_kernel_available, is_peft_available from ..data_utils import apply_chat_template, extract_prompt, is_conversational, prepare_multimodal_messages from ..models import get_act_offloading_ctx_manager, prepare_deepspeed, prepare_fsdp from ..models.utils import disable_gradient_checkpointing from .base_trainer import _BaseTrainer from .callbacks import SyncRefModelCallback from .dpo_config import DPOConfig from .utils import ( create_model_from_path, disable_dropout_in_model, entropy_from_logits, flush_left, flush_right, get_config_model_id, hash_module, pad, remove_none_values, selective_log_softmax, use_adapter, ) if is_peft_available(): from peft import PeftConfig, PeftModel, get_peft_model if is_liger_kernel_available(): from liger_kernel.chunked_loss import LigerFusedLinearDPOLoss logger = get_logger(__name__) FLASH_ATTENTION_VARIANTS = { "flash_attention_2", "flash_attention_3", "kernels-community/flash-attn2", "kernels-community/flash-attn3", "kernels-community/vllm-flash-attn3", } def get_dataset_column_names(dataset: Dataset | IterableDataset) -> list[str]: return list(next(iter(dataset)).keys()) if dataset.column_names is None else dataset.column_names @dataclass class DataCollatorForPreference(DataCollatorMixin): """ Data collator used for preference data. Inputs are dynamically padded to the maximum length of a batch. This collator expects each example in the input list to be a dictionary containing the keys `"prompt_ids"`, `"chosen_ids"` and `"rejected_ids"`. The collator returns a dictionary containing the following keys: - `"input_ids"`: Tensor of input IDs, padded to the maximum length of the batch. The first half of the batch corresponds to the `"chosen_ids"` and the second half to the `"rejected_ids"`. - `"attention_mask"`: Tensor of attention mask, padded to the maximum length of the batch. - `"completion_mask"`: Tensor indicating the positions of the completion tokens, padded to the maximum length of the batch. Optionally, the examples can contain a `"ref_chosen_logps"` and `"ref_rejected_logps"` keys, in which case the returned dictionary will also contain these keys with the corresponding tensors. Args: pad_token_id (`int`): Token ID to use for padding. max_length (`int`, *optional*): Maximum length of the sequences after concatenation. Sequences longer than `max_length` are truncated before padding, which avoids allocating oversized tensors for batches containing very long sequences. truncation_mode (`str`, *optional*, defaults to `"keep_start"`): Truncation mode when a concatenated sequence exceeds `max_length`. Possible values are `"keep_end"` and `"keep_start"`. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. return_tensors (`str`, *optional*, defaults to `"pt"`): Type of Tensor to return. Only `"pt"` is currently supported. Examples: ```python >>> from trl.trainer.dpo_trainer import DataCollatorForPreference >>> collator = DataCollatorForPreference(pad_token_id=0) >>> examples = [ ... {"prompt_ids": [1, 2, 3], "chosen_ids": [4, 5], "rejected_ids": [6]}, ... {"prompt_ids": [7, 8], "chosen_ids": [9], "rejected_ids": [10, 11]}, ... ] >>> collator(examples) {'input_ids': tensor([[ 1, 2, 3, 4, 5], [ 7, 8, 9, 0, 0], [ 1, 2, 3, 6, 0], [ 7, 8, 10, 11, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1], [1, 1, 1, 0, 0], [1, 1, 1, 1, 0], [1, 1, 1, 1, 0]]), 'completion_mask': tensor([[0, 0, 0, 1, 1], [0, 0, 1, 0, 0], [0, 0, 0, 1, 0], [0, 0, 1, 1, 0]])} ``` """ pad_token_id: int max_length: int | None = None truncation_mode: str = "keep_start" pad_to_multiple_of: int | None = None return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: prompt_chosen_ids = [example["prompt_ids"] + example["chosen_ids"] for example in examples] prompt_rejected_ids = [example["prompt_ids"] + example["rejected_ids"] for example in examples] chosen_mask = [[0] * len(example["prompt_ids"]) + [1] * len(example["chosen_ids"]) for example in examples] rejected_mask = [[0] * len(example["prompt_ids"]) + [1] * len(example["rejected_ids"]) for example in examples] if self.max_length is not None: if self.truncation_mode == "keep_start": prompt_chosen_ids = [ids[: self.max_length] for ids in prompt_chosen_ids] prompt_rejected_ids = [ids[: self.max_length] for ids in prompt_rejected_ids] chosen_mask = [m[: self.max_length] for m in chosen_mask] rejected_mask = [m[: self.max_length] for m in rejected_mask] elif self.truncation_mode == "keep_end": prompt_chosen_ids = [ids[-self.max_length :] for ids in prompt_chosen_ids] prompt_rejected_ids = [ids[-self.max_length :] for ids in prompt_rejected_ids] chosen_mask = [m[-self.max_length :] for m in chosen_mask] rejected_mask = [m[-self.max_length :] for m in rejected_mask] chosen_attention_mask = [[1] * len(ids) for ids in prompt_chosen_ids] rejected_attention_mask = [[1] * len(ids) for ids in prompt_rejected_ids] input_ids = prompt_chosen_ids + prompt_rejected_ids attention_mask = chosen_attention_mask + rejected_attention_mask completion_mask = chosen_mask + rejected_mask # Convert to tensor input_ids = [torch.tensor(ids) for ids in input_ids] attention_mask = [torch.tensor(m, dtype=torch.long) for m in attention_mask] completion_mask = [torch.tensor(m, dtype=torch.long) for m in completion_mask] if "ref_chosen_logps" in examples[0]: ref_chosen_logps = torch.tensor([example["ref_chosen_logps"] for example in examples]) if "ref_rejected_logps" in examples[0]: ref_rejected_logps = torch.tensor([example["ref_rejected_logps"] for example in examples]) # Pad output = {} output["input_ids"] = pad( input_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) output["attention_mask"] = pad( attention_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) output["completion_mask"] = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) if "ref_chosen_logps" in examples[0]: output["ref_chosen_logps"] = ref_chosen_logps if "ref_rejected_logps" in examples[0]: output["ref_rejected_logps"] = ref_rejected_logps return output @dataclass class DataCollatorForVisionPreference(DataCollatorMixin): """ Data collator for vision-preference tasks. Unlike text-only datasets, where the collator typically receives pre-tokenized inputs ready for batching, vision-language data processing involves converting images into pixel values. This conversion is disk-intensive, making upfront preprocessing of the entire dataset impractical. Therefore, this collator performs tokenization and image processing on-the-fly to efficiently prepare batches. Each input example should be a dictionary containing at least: - An `"images"` key holding a list of images, or an `"image"` key holding a single image. - Keys `"prompt"` `"chosen"` and `"rejected"` for the prompt and preference responses. The collator outputs a dictionary including: - `"input_ids"`: Tensor of token IDs. - `"attention_mask"`: Tensor indicating attention mask. - `"completion_mask"`: Tensor indicating which tokens correspond to completions. - `"pixel_values"`: Tensor representing image pixel values. Additional keys may be present depending on the processor, such as `"image_grid_thw"`. Args: processor ([`~transformers.ProcessorMixin`]): The processor used to tokenize text and process images. It must be a subclass of [`~transformers.ProcessorMixin`] and include a `tokenizer` with a defined `pad_token_id`. max_length (`int`, *optional*): Maximum sequence length. Sequences longer than `max_length` are truncated before padding, which avoids allocating oversized tensors for batches containing very long sequences. Only `"keep_start"` truncation applies to vision datasets; `"keep_end"` is rejected upstream. pad_to_multiple_of (`int` or `None`, optional, defaults to `None`): If set, the sequences will be padded to a multiple of this value. return_tensors (`str`, optional, defaults to `"pt"`): The tensor type to return. Currently, only `"pt"` (PyTorch tensors) is supported. Example: ```python >>> from trl.trainer.dpo_trainer import DataCollatorForVisionPreference >>> from transformers import AutoProcessor >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") >>> collator = DataCollatorForVisionPreference(processor) >>> examples = [ ... { ... "images": [Image.open("image_0.png")], ... "prompt": [{"role": "user", "content": "What is this?"}], ... "chosen": [{"role": "assistant", "content": "This is a cat."}], ... "rejected": [{"role": "assistant", "content": "This is a dog."}], ... }, ... { ... "images": [Image.open("image_1.png")], ... "prompt": [{"role": "user", "content": "Describe this image."}], ... "chosen": [{"role": "assistant", "content": "A beautiful landscape."}], ... "rejected": [{"role": "assistant", "content": "An urban cityscape."}], ... }, ... ] >>> collator(examples) {'input_ids': tensor([[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 3838, 374, 419, 30, 151645, 198, 151644, 77091, 198, 1986, 374, 264, 8251, 13, 151645, 198], [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 74785, 419, 2168, 13, 151645, 198, 151644, 77091, 198, 32, 6233, 18414, 13, 151645, 198, 151643], [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 3838, 374, 419, 30, 151645, 198, 151644, 77091, 198, 1986, 374, 264, 5562, 13, 151645, 198], [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 74785, 419, 2168, 13, 151645, 198, 151644, 77091, 198, 2082, 15662, 3283, 57518, 13, 151645, 198]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'pixel_values': tensor([[-1.3251, 0.1347, -0.4784, ..., 0.4537, -0.0156, 1.2358], [ 0.5727, 0.4997, -0.9164, ..., -0.5701, 0.7950, -0.7123], [-0.0550, -0.8288, 1.0690, ..., -0.1293, -0.1151, 1.6055], ..., [ 0.2953, 0.5581, 0.1785, ..., -0.7123, -0.7977, 0.1693], [-0.7558, 1.0398, 1.3464, ..., -0.5417, -0.5417, 0.4395], [ 0.8063, 0.6895, 0.4267, ..., -0.4422, 1.3354, 0.1266]]), 'image_grid_thw': tensor([[1, 4, 4], [1, 4, 4], [1, 4, 4], [1, 4, 4]]), 'completion_mask': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]])} ``` """ processor: ProcessorMixin max_length: int | None = None pad_to_multiple_of: int | None = None return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: if self.pad_to_multiple_of is not None: raise NotImplementedError( "Padding to a multiple of a value is not yet implemented for vision-language modeling and " "prompt-completion data." ) if "image" in examples[0]: for example in examples: example["images"] = [example.pop("image")] images = [example["images"] for example in examples] * 2 # repeat for chosen and rejected # Transformers requires at least one image in the batch, otherwise it throws an error if all(img_list == [] for img_list in images): images = None if is_conversational(examples[0]): # conversational case for example in examples: example["prompt"] = prepare_multimodal_messages(example["prompt"], images=example["images"]) example["chosen"] = prepare_multimodal_messages(example["chosen"], images=[]) example["rejected"] = prepare_multimodal_messages(example["rejected"], images=[]) examples = [apply_chat_template(example, self.processor) for example in examples] prompts = [example["prompt"] for example in examples] * 2 # repeat for chosen and rejected chosens = [example["chosen"] for example in examples] rejecteds = [example["rejected"] for example in examples] processed_prompts = self.processor( images=images, text=prompts, padding=True, padding_side="left", return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) processed_chosens = self.processor( text=chosens, padding=True, padding_side="right", return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) processed_rejecteds = self.processor( text=rejecteds, padding=True, padding_side="right", return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) # Concatenate prompts and completions prompt_ids, prompt_mask = processed_prompts["input_ids"], processed_prompts["attention_mask"] chosen_ids, chosen_mask = processed_chosens["input_ids"], processed_chosens["attention_mask"] rejected_ids, rejected_mask = processed_rejecteds["input_ids"], processed_rejecteds["attention_mask"] pad_token_id = self.processor.tokenizer.pad_token_id or self.processor.tokenizer.eos_token_id completion_ids = torch.cat(tuple(pad([chosen_ids, rejected_ids], padding_value=pad_token_id))) completion_mask = torch.cat(tuple(pad([chosen_mask, rejected_mask], padding_value=0))) input_ids = torch.cat((prompt_ids, completion_ids), dim=1) attention_mask = torch.cat((prompt_mask, completion_mask), dim=1) completion_mask = torch.cat((torch.zeros_like(prompt_mask), completion_mask), dim=1) if "token_type_ids" in processed_prompts: # special case for Gemma prompt_token_type_ids = processed_prompts["token_type_ids"] chosen_type_ids = processed_chosens["token_type_ids"] rejected_type_ids = processed_rejecteds["token_type_ids"] completion_token_type_ids = torch.cat(tuple(pad([chosen_type_ids, rejected_type_ids], padding_value=0))) token_type_ids = torch.cat((prompt_token_type_ids, completion_token_type_ids), dim=1) if "mm_token_type_ids" in processed_prompts: # special case for Qwen2.5-VL prompt_mm_token_type_ids = processed_prompts["mm_token_type_ids"] mm_token_type_ids = torch.cat((prompt_mm_token_type_ids, torch.zeros_like(completion_ids)), dim=1) # Flush left to reduce padding if "token_type_ids" in processed_prompts and "mm_token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, token_type_ids, mm_token_type_ids = flush_left( attention_mask, input_ids, completion_mask, token_type_ids, mm_token_type_ids ) elif "token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, token_type_ids = flush_left( attention_mask, input_ids, completion_mask, token_type_ids ) elif "mm_token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, mm_token_type_ids = flush_left( attention_mask, input_ids, completion_mask, mm_token_type_ids ) else: attention_mask, input_ids, completion_mask = flush_left(attention_mask, input_ids, completion_mask) if self.max_length is not None: input_ids = input_ids[:, : self.max_length] attention_mask = attention_mask[:, : self.max_length] completion_mask = completion_mask[:, : self.max_length] if "token_type_ids" in processed_prompts: token_type_ids = token_type_ids[:, : self.max_length] if "mm_token_type_ids" in processed_prompts: mm_token_type_ids = mm_token_type_ids[:, : self.max_length] # Build the output dictionary output = processed_prompts # we take processed_prompts because it contains the images output["input_ids"] = input_ids output["attention_mask"] = attention_mask output["completion_mask"] = completion_mask if "token_type_ids" in processed_prompts: output["token_type_ids"] = token_type_ids if "mm_token_type_ids" in processed_prompts: output["mm_token_type_ids"] = mm_token_type_ids return output class DPOTrainer(_BaseTrainer): """ Trainer for Direct Preference Optimization (DPO) method. This algorithm was initially proposed in the paper [Direct Preference Optimization: Your Language Model is Secretly a Reward Model](https://huggingface.co/papers/2305.18290). This class is a wrapper around the [`~transformers.Trainer`] class and inherits all of its attributes and methods. Example: ```python from trl import DPOTrainer from datasets import load_dataset dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = DPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=dataset, ) trainer.train() ``` Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `.from_pretrained` (where `` is derived from the model config) with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. - A [`~peft.PeftModel`] object. Only causal language models are supported. ref_model (`PreTrainedModel`, *optional*): Reference model used to compute the reference log probabilities. - If provided, this model is used directly as the reference policy. - If `None`, the trainer will automatically use the initial policy corresponding to `model`, i.e. the model state before DPO training starts. args ([`DPOConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. data_collator ([`~transformers.DataCollator`], *optional*): Function to use to form a batch from a list of elements of the processed `train_dataset` or `eval_dataset`. Will default to [`~trainer.dpo_trainer.DataCollatorForPreference`] if the model is a language model and [`~trainer.dpo_trainer.DataCollatorForVisionPreference`] if the model is a vision-language model. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. This trainer supports both [language modeling](#language-modeling) type and [prompt-completion](#prompt-completion) type. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. The padding side must be set to "left". If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function that will be used to compute metrics at evaluation. Must take a [`~transformers.EvalPrediction`] and return a dictionary string to metric values. When passing [`SFTConfig`] with `batch_eval_metrics` set to `True`, your `compute_metrics` function must take a boolean `compute_result` argument. This will be triggered after the last eval batch to signal that the function needs to calculate and return the global summary statistics rather than accumulating the batch-level statistics. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. """ _tag_names = ["trl", "dpo"] _name = "DPO" _paper = { "title": "Direct Preference Optimization: Your Language Model is Secretly a Reward Model", "id": "2305.18290", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{rafailov2023direct, title = {{Direct Preference Optimization: Your Language Model is Secretly a Reward Model}}, author = {Rafael Rafailov and Archit Sharma and Eric Mitchell and Christopher D. Manning and Stefano Ermon and Chelsea Finn}, year = 2023, booktitle = {Advances in Neural Information Processing Systems 36: Annual Conference on Neural Information Processing Systems 2023, NeurIPS 2023, New Orleans, LA, USA, December 10 - 16, 2023}, url = {http://papers.nips.cc/paper_files/paper/2023/hash/a85b405ed65c6477a4fe8302b5e06ce7-Abstract-Conference.html}, editor = {Alice Oh and Tristan Naumann and Amir Globerson and Kate Saenko and Moritz Hardt and Sergey Levine}, }"""), } def __init__( self, model: "str | PreTrainedModel | PeftModel", ref_model: PreTrainedModel | None = None, args: DPOConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), peft_config: "PeftConfig | None" = None, ): # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = DPOConfig(f"{model_name}-DPO") if train_dataset is None: raise ValueError("`train_dataset` is required") elif isinstance(train_dataset, IterableDataset): # IterableDataset requires dispatch_batches=False because Accelerate's dispatch mode may try to concatenate # batches from multiple processes, leading to mismatch errors. if args.accelerator_config.dispatch_batches is True: logger.warning( "You are using an `IterableDataset` for training with `dispatch_batches=True`. `dispatch_batches` " "is forced to `False` when using an `IterableDataset`. To remove this warning, unset " "`dispatch_batches` in `DPOConfig` or set it to `False`." ) args.accelerator_config.dispatch_batches = False # Model if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model = create_model_from_path(model, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the `DPOConfig`, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) if ref_model is model: raise ValueError( "`model` and `ref_model` cannot be the same object. In most cases you should omit `ref_model` and " "we'll initialize it to a copy of `model` for you." ) # Processing class if processing_class is None: processing_class = AutoProcessor.from_pretrained(get_config_model_id(model.config)) # Handle pad token for processors or tokenizers if isinstance(processing_class, ProcessorMixin): tokenizer = processing_class.tokenizer self._is_vlm = True elif isinstance(processing_class, PreTrainedTokenizerBase): tokenizer = processing_class self._is_vlm = False else: raise TypeError("The `processing_class` must be either a `PreTrainedTokenizerBase` or a `ProcessorMixin`") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token self.pad_token = tokenizer.pad_token self.pad_token_id = tokenizer.pad_token_id self.eos_token_id = tokenizer.eos_token_id if is_peft_available() and is_peft_model(model) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge " "and unload the existing adapter, save the resulting base model, and then pass that base model along " "with the new `peft_config` to the trainer." ) if is_peft_available() and is_peft_model(model) and ref_model is None: # If the model is a PEFT model with a pretrained adapter, we need to create a "ref" adapter that is a copy # of the "default" adapter, so that we can use it as the reference model during DPO training. model.add_adapter("ref", model.peft_config["default"]) for name, param in model.named_parameters(): if ".default." in name: ref_name = name.replace(".default.", ".ref.") ref_param = model.get_parameter(ref_name) ref_param.data.copy_(param.data) # Create PEFT model if peft_config is not None: model = get_peft_model(model, peft_config) # When using gradient checkpointing with PEFT, we need to enable input gradients. transformers.Trainer normally # handles this, but a bug currently prevents it; see https://github.com/huggingface/transformers/issues/42489 if is_peft_available() and isinstance(model, PeftModel) and args.gradient_checkpointing: model.enable_input_require_grads() # When using QLoRA, the PEFT adapter weights are converted to bf16 to follow the recommendations from the # original paper (see https://huggingface.co/papers/2305.14314, paragraph 3). Normally, this can be done by # passing `autocast_adapter_dtype=False` to `get_peft_model`, but this option is not yet supported for # quantized models. See: https://github.com/huggingface/peft/issues/2889 # Non-quantized models do not have the `is_loaded_in_{8,4}bit` attributes, whereas quantized models do if getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False): for param in model.parameters(): if param.requires_grad: param.data = param.data.to(torch.bfloat16) # Data collator self.padding_free = args.padding_free if self.padding_free: logger.warning( "`padding_free=True` is temporarily unavailable after a refactor and is currently disabled. Falling " "back to standard padding (`padding_free=False`). This feature is planned to return in a future " "update; for now, please set `padding_free=False` explicitly." ) self.padding_free = False dataset_sample = next(iter(train_dataset)) self._is_vision_dataset = "image" in dataset_sample or "images" in dataset_sample if self._is_vision_dataset and not self._is_vlm: raise ValueError( "The dataset appears to be vision-related (contains 'image' or 'images' keys), but the provided " "model does not seem to be a vision-language model. Please check your model and dataset." ) if self._is_vision_dataset and args.max_length is not None and args.truncation_mode == "keep_end": raise ValueError( "truncation_mode='keep_end' is not supported for vision-language models. Image tokens reside " "inside the prompt portion of the sequence; depending on the example, keep_end may silently " "drop them, causing pixel_values to be forwarded to the model with no corresponding visual " "tokens in input_ids. Use truncation_mode='keep_start' (the default) or set max_length=None." ) if data_collator is None and not self._is_vision_dataset: # Get the pad token: if not provided, use the one from the processing class or the eos token # if the processing class does not have a pad token. pad_token = args.pad_token or tokenizer.pad_token or tokenizer.eos_token pad_token_id = tokenizer.convert_tokens_to_ids(pad_token) if pad_token_id is None: raise ValueError( f"The specified `pad_token` ('{pad_token}') is not found in the vocabulary of the given " f"`processing_class` ({processing_class.__class__.__name__}). Ensure that the `pad_token` exists " "in the vocabulary before using it as a padding token." ) data_collator = DataCollatorForPreference( pad_token_id=pad_token_id, max_length=args.max_length, truncation_mode=args.truncation_mode, pad_to_multiple_of=args.pad_to_multiple_of, ) elif data_collator is None and self._is_vision_dataset: data_collator = DataCollatorForVisionPreference( processor=processing_class, max_length=args.max_length, pad_to_multiple_of=args.pad_to_multiple_of, ) # Training arguments self.beta = args.beta self.precompute_ref_logps = args.precompute_ref_log_probs self.loss_types = args.loss_type # args.loss_type is already a list self.loss_weights = args.loss_weights or [1.0] * len(self.loss_types) self.ld_alpha = args.ld_alpha self.f_divergence_type = args.f_divergence_type self.f_alpha_divergence_coef = args.f_alpha_divergence_coef self.label_smoothing = args.label_smoothing self.use_weighting = args.use_weighting if self.use_weighting and any(loss_type in {"aot", "aot_unpaired"} for loss_type in self.loss_types): raise NotImplementedError( "WPO-style weighting is not implemented for 'aot' or 'aot_unpaired' because those losses sort " "samples, which would misalign per-pair weights." ) if "robust" in self.loss_types and not (0.0 <= self.label_smoothing < 0.5): logger.warning( "The `label_smoothing` parameter should lie in [0.0, 0.5) for the 'robust' loss. You provided " f"{self.label_smoothing}." ) if "exo_pair" in self.loss_types and self.label_smoothing == 0.0: raise ValueError( "Label smoothing must be greater than 0.0 when using 'exo_pair' loss. The EXO paper recommends a " "value of 1e-3." ) self.use_liger_kernel = args.use_liger_kernel if args.use_liger_kernel: if not is_liger_kernel_available(): raise ImportError( "You set `use_liger_kernel=True` but the liger kernel is not available. " "Please install liger-kernel first: `pip install liger-kernel`" ) if len(self.loss_types) != 1: raise NotImplementedError( "Multiple loss types are not yet supported when using Liger kernel. If you need this feature, " "please open a feature request at https://github.com/huggingface/trl/issues." ) self.liger_loss_fn = LigerFusedLinearDPOLoss(beta=args.beta, loss_type=self.loss_types[0]) if compute_metrics is not None: raise ValueError( "compute_metrics is not supported with the Liger kernel. compute_metrics requires to be able to " "recover the logits from the forward pass, but Liger kernel does not materialize logits." ) if self.precompute_ref_logps: raise ValueError( "Liger DPO loss does not support precomputing reference log probabilities. Either disable " "`precompute_ref_log_probs` or set `use_liger_kernel` to False." ) # Dataset # Skip dataset preparation if it's a VLM, where preprocessing (e.g., image-to-pixel conversion) is too costly # and done on the fly instead. skip_prepare_dataset = self._is_vision_dataset if not skip_prepare_dataset: train_dataset = self._prepare_dataset(train_dataset, processing_class, args, "train") if eval_dataset is not None: if isinstance(eval_dataset, dict): eval_dataset = { key: self._prepare_dataset(dataset, processing_class, args, key) for key, dataset in eval_dataset.items() } else: eval_dataset = self._prepare_dataset(eval_dataset, processing_class, args, "eval") # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, ) # Initialize activation offloading context if self.args.activation_offloading: self.maybe_activation_offload_context = get_act_offloading_ctx_manager(model=self.model) else: self.maybe_activation_offload_context = contextlib.nullcontext() # Reference model if ref_model is None: if is_peft_model(self.model): # If PEFT is used, the reference model is not needed since the adapter can be disabled to revert to the # initial model. self.ref_model = None else: ref_model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if self.args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: ref_model_init_kwargs["device_map"] = None ref_model_path = get_config_model_id(self.model.config) self.ref_model = create_model_from_path(ref_model_path, **ref_model_init_kwargs) else: self.ref_model = ref_model # Disable dropout in the models if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 # Add tags to the model self.model.add_model_tags(self._tag_names) if self.ref_model is not None: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) elif self.is_fsdp_enabled: self.ref_model = prepare_fsdp(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) if args.sync_ref_model: if self.ref_model is None: raise NotImplementedError( "You passed `sync_ref_model=True` while using a PEFT model, which is currently not supported. " "With PEFT, DPOTrainer does not keep a separate reference model in memory; instead, it recovers " "reference behavior by temporarily disabling the adapter. As a result, there is no standalone " "`ref_model` instance to synchronize. Use `sync_ref_model=False`, or opt for full fine-tuning if " "you need a synced reference model. If you need `sync_ref_model` to work with PEFT, please open a " "feature request at https://github.com/huggingface/trl/issues." ) if args.precompute_ref_log_probs: raise ValueError( "You cannot use `sync_ref_model=True` together with `precompute_ref_log_probs=True`. " "`precompute_ref_log_probs=True` assumes a fixed reference model, but with `sync_ref_model=True` " "the reference model is periodically updated during training, making any precomputed reference " "log-probs stale. Set `precompute_ref_log_probs=False` or disable `sync_ref_model`." ) self.add_callback(SyncRefModelCallback(ref_model=self.ref_model, accelerator=self.accelerator)) if args.precompute_ref_log_probs: if isinstance(self.train_dataset, IterableDataset) or isinstance( self.eval_dataset, (IterableDataset, IterableDatasetDict) ): raise ValueError( "`precompute_ref_log_probs=True` is not supported with IterableDataset. Please use a map-style " "Dataset or set `precompute_ref_log_probs=False`." ) batch_size = self.args.precompute_ref_batch_size or self.args.per_device_train_batch_size self.train_dataset = self._precompute_ref_logps(self.train_dataset, "train", batch_size) if self.eval_dataset is not None: batch_size = self.args.precompute_ref_batch_size or self.args.per_device_eval_batch_size if isinstance(self.eval_dataset, dict): self.eval_dataset = { name: self._precompute_ref_logps(dataset, name, batch_size) for name, dataset in self.eval_dataset.items() } else: self.eval_dataset = self._precompute_ref_logps(self.eval_dataset, "eval", batch_size) def _prepare_dataset( self, dataset: Dataset | IterableDataset, processing_class: PreTrainedTokenizerBase | ProcessorMixin, args: DPOConfig, dataset_name: str, ) -> Dataset | IterableDataset: # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from # sampled data. if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` dataset = dataset.with_transform(remove_none_values) # Build the kwargs for the `map` function map_kwargs = {} if isinstance(dataset, Dataset): # IterableDataset does not support num_proc map_kwargs["num_proc"] = args.dataset_num_proc with PartialState().main_process_first(): # Extract the prompt if needed first_example = next(iter(dataset)) if "prompt" not in first_example: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Extracting prompt from {dataset_name} dataset" dataset = dataset.map(extract_prompt, **map_kwargs) # Apply the chat template if needed first_example = next(iter(dataset)) if not is_conversational(first_example): if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Adding EOS to {dataset_name} dataset" def add_eos(example, eos_token): if not example["chosen"].endswith(eos_token): example["chosen"] = example["chosen"] + eos_token if not example["rejected"].endswith(eos_token): example["rejected"] = example["rejected"] + eos_token return example eos_token = processing_class.tokenizer.eos_token if self._is_vlm else processing_class.eos_token dataset = dataset.map(add_eos, fn_kwargs={"eos_token": eos_token}, **map_kwargs) # Tokenize the dataset if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset" def tokenize_fn(example, processing_class): tools = example.get("tools") tools = json.loads(tools) if isinstance(tools, str) else tools output = {} if is_conversational(example): if self._is_vlm: prompt = prepare_multimodal_messages(example["prompt"], images=[]) chosen = prepare_multimodal_messages(example["chosen"], images=[]) rejected = prepare_multimodal_messages(example["rejected"], images=[]) else: prompt = example["prompt"] chosen = example["chosen"] rejected = example["rejected"] prompt_ids = processing_class.apply_chat_template( prompt, tools=tools, add_generation_prompt=True, tokenize=True, return_dict=False, **example.get("chat_template_kwargs", {}), ) prompt_chosen_processed = processing_class.apply_chat_template( prompt + chosen, tools=tools, tokenize=True, return_dict=True, **example.get("chat_template_kwargs", {}), ) prompt_rejected_processed = processing_class.apply_chat_template( prompt + rejected, tools=tools, tokenize=True, return_dict=True, **example.get("chat_template_kwargs", {}), ) # Fix transformers inconsistency: for VLMs, apply_chat_template returns lists of lists # even for single examples, while for LLMs it returns lists of ints. prompt_ids = prompt_ids[0] if isinstance(prompt_ids[0], list) else prompt_ids prompt_chosen_processed = { k: v[0] if isinstance(v[0], list) else v for k, v in prompt_chosen_processed.items() } prompt_rejected_processed = { k: v[0] if isinstance(v[0], list) else v for k, v in prompt_rejected_processed.items() } prompt_chosen_ids = prompt_chosen_processed["input_ids"] prompt_rejected_ids = prompt_rejected_processed["input_ids"] else: prompt_ids = processing_class(text=example["prompt"])["input_ids"] prompt_chosen_ids = processing_class(text=example["prompt"] + example["chosen"])["input_ids"] prompt_rejected_ids = processing_class(text=example["prompt"] + example["rejected"])["input_ids"] # Fix transformers inconsistency: for VLMs, processing_class returns lists of lists # even for single examples, while for LLMs it returns lists of ints. prompt_ids = prompt_ids[0] if isinstance(prompt_ids[0], list) else prompt_ids prompt_chosen_ids = ( prompt_chosen_ids[0] if isinstance(prompt_chosen_ids[0], list) else prompt_chosen_ids ) prompt_rejected_ids = ( prompt_rejected_ids[0] if isinstance(prompt_rejected_ids[0], list) else prompt_rejected_ids ) # Check if the tokenized prompt starts with the tokenized prompt+completion if not prompt_chosen_ids[: len(prompt_ids)] == prompt_ids: logger.warning( "Mismatch between tokenized prompt and the start of tokenized prompt+chosen. " "This may be due to unexpected tokenizer behavior, whitespace issues, or special " "token handling. Verify that the tokenizer is processing text consistently." ) if not prompt_rejected_ids[: len(prompt_ids)] == prompt_ids: logger.warning( "Mismatch between tokenized prompt and the start of tokenized prompt+rejected. " "This may be due to unexpected tokenizer behavior, whitespace issues, or special " "token handling. Verify that the tokenizer is processing text consistently." ) output["prompt_ids"] = prompt_ids output["chosen_ids"] = prompt_chosen_ids[len(prompt_ids) :] output["rejected_ids"] = prompt_rejected_ids[len(prompt_ids) :] return output dataset = dataset.map(tokenize_fn, fn_kwargs={"processing_class": processing_class}, **map_kwargs) return dataset def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). if self._signature_columns is None: if self._is_vision_dataset: self._signature_columns = [ "prompt", "chosen", "rejected", "image", "images", "tools", "chat_template_kwargs", ] else: self._signature_columns = [ "prompt_ids", "chosen_ids", "rejected_ids", "ref_chosen_logps", "ref_rejected_logps", ] def _precompute_ref_logps(self, dataset: Dataset, name: str, batch_size: int) -> Dataset: model_hash = hash_module(self.ref_model or self.model) fingerprint = Hasher.hash((dataset._fingerprint, model_hash)) cache_file = dataset._get_cache_file_path(fingerprint).removesuffix(".arrow") + ".npz" if os.path.exists(cache_file): loaded = np.load(cache_file) ref_chosen_logps = loaded["ref_chosen_logps"] ref_rejected_logps = loaded["ref_rejected_logps"] else: dataloader = DataLoader( dataset, batch_size=batch_size, collate_fn=self.data_collator, num_workers=self.args.dataloader_num_workers, pin_memory=self.args.dataloader_pin_memory, shuffle=False, ) data_loader = self.accelerator.prepare(dataloader) ref_chosen_logps = [] ref_rejected_logps = [] for padded_batch in tqdm(iterable=data_loader, desc=f"Computing reference log probs for {name} dataset"): ref_chosen_logp, ref_rejected_logp = self.compute_ref_log_probs(padded_batch) ref_chosen_logp, ref_rejected_logp = self.accelerator.gather_for_metrics( (ref_chosen_logp, ref_rejected_logp) ) ref_chosen_logps.append(ref_chosen_logp.cpu()) ref_rejected_logps.append(ref_rejected_logp.cpu()) # Save the reference log probabilities to cache. We need .float() because bf16 is not supported by numpy ref_chosen_logps = torch.cat(ref_chosen_logps).float().numpy() ref_rejected_logps = torch.cat(ref_rejected_logps).float().numpy() if self.accelerator.is_main_process: np.savez_compressed( cache_file, ref_chosen_logps=ref_chosen_logps, ref_rejected_logps=ref_rejected_logps ) self.accelerator.wait_for_everyone() dataset = dataset.add_column(name="ref_chosen_logps", column=ref_chosen_logps) dataset = dataset.add_column(name="ref_rejected_logps", column=ref_rejected_logps, new_fingerprint=fingerprint) return dataset def _truncate_inputs( self, input_ids: torch.Tensor, attention_mask: torch.Tensor, completion_mask: torch.Tensor, *extra: torch.Tensor, ) -> tuple[torch.Tensor, ...]: if self.args.max_length is None: return input_ids, attention_mask, completion_mask, *extra if self.args.truncation_mode == "keep_start": input_ids = input_ids[:, : self.args.max_length] attention_mask = attention_mask[:, : self.args.max_length] completion_mask = completion_mask[:, : self.args.max_length] extra = tuple(t[:, : self.args.max_length] for t in extra) elif self.args.truncation_mode == "keep_end": attention_mask, input_ids, completion_mask, *extra = flush_right( attention_mask, input_ids, completion_mask, *extra ) input_ids = input_ids[:, -self.args.max_length :] attention_mask = attention_mask[:, -self.args.max_length :] completion_mask = completion_mask[:, -self.args.max_length :] extra = tuple(t[:, -self.args.max_length :] for t in extra) attention_mask, input_ids, completion_mask, *extra = flush_left( attention_mask, input_ids, completion_mask, *extra ) extra = tuple(extra) else: raise ValueError( f"Unsupported truncation mode: {self.args.truncation_mode}, expected 'keep_start' or 'keep_end'" ) return input_ids, attention_mask, completion_mask, *extra def compute_ref_log_probs(self, inputs): """Computes reference log probabilities for a single padded batch.""" device = self.accelerator.device input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] completion_mask = inputs["completion_mask"] # token_type_ids and mm_token_type_ids are sequence-length-aligned: truncate to match input_ids extra_keys = [k for k in ("token_type_ids", "mm_token_type_ids") if k in inputs] input_ids, attention_mask, completion_mask, *extra = self._truncate_inputs( input_ids, attention_mask, completion_mask, *[inputs[k] for k in extra_keys] ) shift_labels = input_ids[..., 1:].contiguous() shift_completion_mask = completion_mask[..., 1:].contiguous() model_kwargs = {"input_ids": input_ids, "attention_mask": attention_mask, "use_cache": False} for key, val in zip(extra_keys, extra, strict=False): model_kwargs[key] = val for key in ("pixel_values", "pixel_attention_mask", "image_grid_thw", "image_sizes"): if key in inputs: model_kwargs[key] = inputs[key] with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): if is_peft_model(self.model) and self.ref_model is None: model = self.accelerator.unwrap_model(self.model) with use_adapter(model, adapter_name="ref" if "ref" in model.peft_config else None): ref_outputs = self.model(**model_kwargs) else: ref_outputs = self.ref_model(**model_kwargs) ref_shift_logits = ref_outputs.logits[..., :-1, :].contiguous() ref_per_token_logps = selective_log_softmax(ref_shift_logits, shift_labels) ref_per_token_logps[shift_completion_mask == 0] = 0.0 if self.ld_alpha is None: ref_logps = ref_per_token_logps.sum(dim=1) else: comp_pos = shift_completion_mask.cumsum(dim=1) comp_lens = shift_completion_mask.sum(dim=1).long() chosen_lens, rejected_lens = comp_lens.chunk(2, dim=0) shared_lens = torch.minimum(chosen_lens, rejected_lens) shared_lens = torch.cat([shared_lens, shared_lens], dim=0).to(device) shared_mask = (comp_pos > 0) & (comp_pos <= shared_lens.unsqueeze(1)) tail_mask = comp_pos > shared_lens.unsqueeze(1) shared_logps = (ref_per_token_logps * shared_mask).sum(dim=1) tail_logps = (ref_per_token_logps * tail_mask).sum(dim=1) ref_logps = shared_logps + self.ld_alpha * tail_logps ref_chosen_logps, ref_rejected_logps = ref_logps.chunk(2, dim=0) return ref_chosen_logps, ref_rejected_logps def _compute_loss_liger(self, model, inputs, return_outputs): if return_outputs: raise RuntimeError( "return_outputs=True is not supported with the Liger DPO loss. The Liger loss computes the loss " "without materializing logits, so outputs cannot be returned." ) mode = "train" if self.model.training else "eval" input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] completion_mask = inputs["completion_mask"] input_ids, attention_mask, completion_mask = self._truncate_inputs(input_ids, attention_mask, completion_mask) decoder = model.get_decoder() outputs = decoder(input_ids, attention_mask=attention_mask, use_cache=False) hidden_states = outputs.last_hidden_state[:, :-1].contiguous() lm_head = model.get_output_embeddings() weight = lm_head.weight bias = lm_head.bias if is_peft_model(model): raise NotImplementedError("Liger DPO loss is not implemented for PEFT models.") else: with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): ref_decoder = self.ref_model.get_decoder() ref_outputs = ref_decoder(input_ids, attention_mask=attention_mask, use_cache=False) ref_lm_head = self.ref_model.get_output_embeddings() ref_hidden_states = ref_outputs.last_hidden_state[:, :-1].contiguous() ref_weight = ref_lm_head.weight ref_bias = ref_lm_head.bias shift_completion_mask = completion_mask[:, 1:].contiguous() labels = input_ids[:, 1:].clone() labels[shift_completion_mask == 0] = -100 loss, metrics = self.liger_loss_fn( weight, hidden_states, labels, bias, ref_hidden_states, ref_weight, ref_bias ) ( chosen_logps, rejected_logps, chosen_logits_mean, rejected_logits_mean, nll_loss, chosen_rewards, rejected_rewards, ) = metrics if mode == "train": num_tokens_in_batch = self.accelerator.gather_for_metrics(inputs["attention_mask"].sum()).sum().item() self._total_train_tokens += num_tokens_in_batch self._metrics[mode]["num_tokens"] = [self._total_train_tokens] avg_chosen_logits = self.accelerator.gather_for_metrics(chosen_logits_mean).mean().item() avg_rejected_logits = self.accelerator.gather_for_metrics(rejected_logits_mean).mean().item() self._metrics[mode]["logits/chosen"].append(avg_chosen_logits) self._metrics[mode]["logits/rejected"].append(avg_rejected_logits) agg_chosen_rewards = self.accelerator.gather(chosen_rewards) agg_rejected_rewards = self.accelerator.gather(rejected_rewards) self._metrics[mode]["rewards/chosen"].append(agg_chosen_rewards.mean().item()) self._metrics[mode]["rewards/rejected"].append(agg_rejected_rewards.mean().item()) reward_accuracies = (chosen_rewards > rejected_rewards).float() agg_reward_accuracies = self.accelerator.gather(reward_accuracies) self._metrics[mode]["rewards/accuracies"].append(agg_reward_accuracies.mean().item()) margins = chosen_rewards - rejected_rewards agg_margins = self.accelerator.gather(margins) self._metrics[mode]["rewards/margins"].append(agg_margins.mean().item()) self._metrics[mode]["logps/chosen"].append(self.accelerator.gather(chosen_logps).mean().item()) self._metrics[mode]["logps/rejected"].append(self.accelerator.gather(rejected_logps).mean().item()) return loss def _compute_loss(self, model, inputs, return_outputs): mode = "train" if self.model.training else "eval" device = self.accelerator.device input_ids = inputs["input_ids"] attention_mask = inputs["attention_mask"] completion_mask = inputs["completion_mask"] # token_type_ids and mm_token_type_ids are sequence-length-aligned: truncate to match input_ids extra_keys = [k for k in ("token_type_ids", "mm_token_type_ids") if k in inputs] input_ids, attention_mask, completion_mask, *extra = self._truncate_inputs( input_ids, attention_mask, completion_mask, *[inputs[k] for k in extra_keys] ) model_kwargs = {"input_ids": input_ids, "attention_mask": attention_mask, "use_cache": False} for key, val in zip(extra_keys, extra, strict=False): model_kwargs[key] = val for key in ("pixel_values", "pixel_attention_mask", "image_grid_thw", "image_sizes"): if key in inputs: model_kwargs[key] = inputs[key] outputs = model(**model_kwargs) shift_logits = outputs.logits[..., :-1, :].contiguous() shift_labels = input_ids[..., 1:].contiguous() shift_completion_mask = completion_mask[..., 1:].contiguous() per_token_logps = selective_log_softmax(shift_logits, shift_labels) per_token_logps[shift_completion_mask == 0] = 0.0 # mask out non-completion tokens if self.ld_alpha is None: logps = per_token_logps.sum(dim=1) # sum over sequence length else: comp_pos = shift_completion_mask.cumsum(dim=1) comp_lens = shift_completion_mask.sum(dim=1).long() chosen_lens, rejected_lens = comp_lens.chunk(2, dim=0) shared_lens = torch.minimum(chosen_lens, rejected_lens) shared_lens = torch.cat([shared_lens, shared_lens], dim=0).to(device) shared_mask = (comp_pos > 0) & (comp_pos <= shared_lens.unsqueeze(1)) # shared: 1 <= pos <= shared_len tail_mask = comp_pos > shared_lens.unsqueeze(1) # tail: pos > shared_len shared_logps = (per_token_logps * shared_mask).sum(dim=1) tail_logps = (per_token_logps * tail_mask).sum(dim=1) logps = shared_logps + self.ld_alpha * tail_logps chosen_logps, rejected_logps = logps.chunk(2, dim=0) # batch is [chosen, rejected] if self.precompute_ref_logps: ref_chosen_logps, ref_rejected_logps = inputs["ref_chosen_logps"], inputs["ref_rejected_logps"] else: # When gradient checkpointing is enabled with use_reentrant=True (default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): if is_peft_model(model) and self.ref_model is None: # When training a PEFT adapter, how we obtain the reference depends on the setup: # - New adapter: disabling adapters yields the base model. # - Re-training an existing adapter: an initial copy is loaded under the name "ref". model = self.accelerator.unwrap_model(model) with use_adapter(model, adapter_name="ref" if "ref" in model.peft_config else None): ref_outputs = self.model(**model_kwargs) else: ref_outputs = self.ref_model(**model_kwargs) ref_shift_logits = ref_outputs.logits[..., :-1, :].contiguous() ref_per_token_logps = selective_log_softmax(ref_shift_logits, shift_labels) ref_per_token_logps[shift_completion_mask == 0] = 0.0 # mask out non-completion tokens if self.ld_alpha is None: ref_logps = ref_per_token_logps.sum(dim=1) # sum over sequence length else: # reuse comp_pos/shared_mask/tail_mask computed above (they depend only on completion_mask) ref_shared_logps = (ref_per_token_logps * shared_mask).sum(dim=1) ref_tail_logps = (ref_per_token_logps * tail_mask).sum(dim=1) ref_logps = ref_shared_logps + self.ld_alpha * ref_tail_logps ref_chosen_logps, ref_rejected_logps = ref_logps.chunk(2, dim=0) # batch is [chosen, rejected] # Get the log ratios for the chosen and rejected responses chosen_logratios = chosen_logps - ref_chosen_logps rejected_logratios = rejected_logps - ref_rejected_logps if self.f_divergence_type == "reverse_kl": # standard DPO chosen_scores = chosen_logratios rejected_scores = rejected_logratios elif self.f_divergence_type == "forward_kl": # f'(t) = 1 - 1/t -> drop constant -> -exp(-logratio) chosen_scores = -torch.exp(-chosen_logratios) rejected_scores = -torch.exp(-rejected_logratios) elif self.f_divergence_type == "js_divergence": # f'(t) = log(2t/(t+1)) -> drop log 2 chosen_scores = F.logsigmoid(chosen_logratios) rejected_scores = F.logsigmoid(rejected_logratios) elif self.f_divergence_type == "alpha_divergence": # alpha-divergence: f'(t) = (t^(α-1) - 1)/(α-1) if abs(self.f_alpha_divergence_coef - 1.0) < 1e-6: # limit case f'(t) -> log(t), fall back to reverse_kl chosen_scores = chosen_logratios rejected_scores = rejected_logratios else: coef = 1.0 / (self.f_alpha_divergence_coef - 1.0) t_chosen = (self.f_alpha_divergence_coef - 1.0) * chosen_logratios t_rejected = (self.f_alpha_divergence_coef - 1.0) * rejected_logratios dtype = t_chosen.dtype # Clamp max so exp(.) stays representable after casting back clamp_max = {torch.float16: 11.0, torch.bfloat16: 80.0, torch.float32: 80.0}[dtype] t_chosen_float = torch.clamp(t_chosen.float(), max=clamp_max) t_rejected_float = torch.clamp(t_rejected.float(), max=clamp_max) chosen_scores = torch.exp(t_chosen_float).to(dtype) * coef rejected_scores = torch.exp(t_rejected_float).to(dtype) * coef else: raise ValueError(f"Unknown f_divergence_type: {self.f_divergence_type}") delta_score = chosen_scores - rejected_scores loss = 0.0 for loss_type, loss_weight in zip(self.loss_types, self.loss_weights, strict=True): if loss_type == "sigmoid": per_sequence_loss = -F.logsigmoid(self.beta * delta_score) elif loss_type == "hinge": per_sequence_loss = torch.relu(1 - self.beta * delta_score) elif loss_type == "ipo": # IPO uses sequence-level log-prob differences; in code these are token-summed over the completion, # which makes the squared loss scale with completion length. We therefore normalize by the number of # completion tokens (average per token) to make β/loss comparable across variable lengths. This length # normalization is not explicitly discussed in the IPO paper; we confirmed this choice with the IPO # authors, and the results reported in the paper correspond to this normalized form. chosen_mask, rejected_mask = completion_mask.chunk(2, dim=0) chosen_avg_score = chosen_scores / chosen_mask.sum(dim=1).clamp(min=1.0) rejected_avg_score = rejected_scores / rejected_mask.sum(dim=1).clamp(min=1.0) ipo_delta = chosen_avg_score - rejected_avg_score # (Eq. 17) of the paper where beta is the regularization parameter for the IPO loss, denoted by τ. per_sequence_loss = (ipo_delta - 1 / (2 * self.beta)) ** 2 elif loss_type == "exo_pair": # Implements EXO-pref from the paper https://huggingface.co/papers/2402.00856, (Eq. 16) # Minimize KL(p_fθ || p_rh) for K=2; p_fθ = softmax(βπ * (log πθ − log π_ref)) over {chosen, rejected} # p_rh = [(1−ε), ε]; expanded KL gives the weighted logsigmoid form below epsilon = torch.tensor(self.label_smoothing, device=device) qw = torch.sigmoid(self.beta * delta_score) log_qw = F.logsigmoid(self.beta * delta_score) log_pw = torch.log1p(-epsilon) ql = torch.sigmoid(-self.beta * delta_score) log_ql = F.logsigmoid(-self.beta * delta_score) log_pl = torch.log(epsilon) per_sequence_loss = qw * (log_qw - log_pw) + ql * (log_ql - log_pl) elif loss_type == "nca_pair": chosen_rewards = self.beta * chosen_scores rejected_rewards = self.beta * rejected_scores per_sequence_loss = ( -F.logsigmoid(chosen_rewards) - 0.5 * F.logsigmoid(-chosen_rewards) - 0.5 * F.logsigmoid(-rejected_rewards) ) elif loss_type == "robust": clean_loss_term = -(1 - self.label_smoothing) * F.logsigmoid(self.beta * delta_score) flipped_loss_term = -self.label_smoothing * F.logsigmoid(-self.beta * delta_score) per_sequence_loss = (clean_loss_term - flipped_loss_term) / (1 - 2 * self.label_smoothing) elif loss_type == "bco_pair": chosen_rewards = self.beta * chosen_scores rejected_rewards = self.beta * rejected_scores per_sequence_loss = -F.logsigmoid(chosen_rewards) - F.logsigmoid(-rejected_rewards) elif loss_type == "sppo_hard": # In the paper (https://huggingface.co/papers/2405.00675), SPPO employs a soft probability approach, # estimated using the PairRM score. The probability calculation is conducted outside of the trainer # class. The version described here is the hard probability version, where P in Equation (4.7) of # Algorithm 1 is set to 1 for the winner and 0 for the loser. winner_margin_error = (chosen_scores - 0.5 / self.beta) ** 2 loser_margin_error = (rejected_scores + 0.5 / self.beta) ** 2 per_sequence_loss = winner_margin_error + loser_margin_error elif loss_type == "aot": logratios = chosen_logps - rejected_logps ref_logratios = ref_chosen_logps - ref_rejected_logps logratios_sorted, _ = torch.sort(logratios, dim=0) ref_logratios_sorted, _ = torch.sort(ref_logratios, dim=0) delta = logratios_sorted - ref_logratios_sorted per_sequence_loss = ( -F.logsigmoid(self.beta * delta) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * delta) * self.label_smoothing ) elif loss_type == "aot_unpaired": chosen_logratios_sorted, _ = torch.sort(chosen_logratios, dim=0) rejected_logratios_sorted, _ = torch.sort(rejected_logratios, dim=0) delta = chosen_logratios_sorted - rejected_logratios_sorted per_sequence_loss = ( -F.logsigmoid(self.beta * delta) * (1 - self.label_smoothing) - F.logsigmoid(-self.beta * delta) * self.label_smoothing ) elif loss_type == "apo_zero": # Eqn (7) of the APO paper (https://huggingface.co/papers/2408.06266) # Use this loss when you believe the chosen outputs are better than your model's default output # Increase chosen likelihood and decrease rejected likelihood losses_chosen = 1 - torch.sigmoid(self.beta * chosen_logratios) losses_rejected = torch.sigmoid(self.beta * rejected_logratios) per_sequence_loss = losses_chosen + losses_rejected elif loss_type == "apo_down": # Eqn (8) of the APO paper (https://huggingface.co/papers/2408.06266) # Use this loss when you believe the chosen outputs are worse than your model's default output. # Decrease chosen likelihood and decrease rejected likelihood more losses_chosen = torch.sigmoid(self.beta * chosen_logratios) losses_rejected = 1 - torch.sigmoid(self.beta * delta_score) per_sequence_loss = losses_chosen + losses_rejected elif loss_type == "discopop": # Eqn (5) of the DiscoPOP paper (https://huggingface.co/papers/2406.08414) logits = delta_score * self.beta # Modulate the mixing coefficient based on the log ratio magnitudes log_ratio_modulation = torch.sigmoid(logits / self.args.discopop_tau) logistic_component = -F.logsigmoid(logits) exp_component = torch.exp(-logits) # Blend between logistic and exponential component based on log ratio modulation per_sequence_loss = ( logistic_component * (1 - log_ratio_modulation) + exp_component * log_ratio_modulation ) elif loss_type == "sft": chosen_logits, _ = shift_logits.chunk(2, dim=0) chosen_labels, _ = shift_labels.chunk(2, dim=0) chosen_mask, _ = shift_completion_mask.chunk(2, dim=0) batch_loss = F.cross_entropy(chosen_logits[chosen_mask.bool()], chosen_labels[chosen_mask.bool()]) # Implementation convenience: expand the scalar SFT loss to a per-sequence tensor so it matches the # shape of other losses; only the mean is used, so this is a no-op numerically. per_sequence_loss = batch_loss.expand(chosen_logits.size(0)) else: raise ValueError( f"Unknown loss type: {loss_type}. Should be one of ['sigmoid', 'hinge', 'ipo', 'exo_pair', " "'nca_pair', 'robust', 'bco_pair', 'sppo_hard', 'aot', 'aot_unpaired', 'apo_zero', 'apo_down', " "'discopop', 'sft']" ) if self.use_weighting: # Eq (2) of the WPO paper: https://huggingface.co/papers/2406.11827 completion_lengths = shift_completion_mask.sum(dim=1).clamp_min(1) with torch.no_grad(): lse1 = torch.logsumexp(shift_logits, dim=-1) lse2 = torch.logsumexp(2.0 * shift_logits, dim=-1) log_denom = lse2 - 2.0 * lse1 aligned_logps = (per_token_logps - log_denom) * shift_completion_mask mean_logps = aligned_logps.sum(dim=1) / completion_lengths weights = torch.exp(mean_logps) chosen_weights, rejected_weights = weights.chunk(2, dim=0) per_sequence_loss *= chosen_weights * rejected_weights loss += per_sequence_loss.mean() * loss_weight # Log the metrics # Entropy per_token_entropy = entropy_from_logits(shift_logits.detach()) entropy = per_token_entropy[shift_completion_mask.bool()].mean() entropy = self.accelerator.gather_for_metrics(entropy).mean().item() self._metrics[mode]["entropy"].append(entropy) # Number of tokens if mode == "train": num_tokens_in_batch = self.accelerator.gather_for_metrics(inputs["attention_mask"].sum()).sum().item() self._total_train_tokens += num_tokens_in_batch self._metrics[mode]["num_tokens"] = [self._total_train_tokens] # Average logits for chosen and rejected completions chosen_logits, rejected_logits = shift_logits.detach().chunk(2, dim=0) chosen_mask, rejected_mask = shift_completion_mask.chunk(2, dim=0) total_chosen_logits = chosen_logits[chosen_mask.bool()].mean(-1).sum() total_chosen_tokens = chosen_mask.sum() total_rejected_logits = rejected_logits[rejected_mask.bool()].mean(-1).sum() total_rejected_tokens = rejected_mask.sum() total_chosen_logits = self.accelerator.gather_for_metrics(total_chosen_logits).sum().item() total_chosen_tokens = self.accelerator.gather_for_metrics(total_chosen_tokens).sum().item() total_rejected_logits = self.accelerator.gather_for_metrics(total_rejected_logits).sum().item() total_rejected_tokens = self.accelerator.gather_for_metrics(total_rejected_tokens).sum().item() avg_chosen_logits = total_chosen_logits / total_chosen_tokens if total_chosen_tokens > 0 else 0.0 avg_rejected_logits = total_rejected_logits / total_rejected_tokens if total_rejected_tokens > 0 else 0.0 self._metrics[mode]["logits/chosen"].append(avg_chosen_logits) self._metrics[mode]["logits/rejected"].append(avg_rejected_logits) # Token accuracy for the chosen completions predictions = chosen_logits.argmax(dim=-1) chosen_mask = shift_completion_mask[: len(shift_completion_mask) // 2].bool() chosen_labels = shift_labels[: len(shift_labels) // 2] correct_predictions = (predictions == chosen_labels) & chosen_mask total_tokens = chosen_mask.sum() correct_tokens = correct_predictions.sum() correct_tokens = self.accelerator.gather_for_metrics(correct_tokens) total_tokens = self.accelerator.gather_for_metrics(total_tokens) total_sum = total_tokens.sum() accuracy = (correct_tokens.sum() / total_sum).item() if total_sum > 0 else 0.0 self._metrics[mode]["mean_token_accuracy"].append(accuracy) # Rewards for chosen and rejected completions chosen_rewards = self.beta * chosen_logratios.detach() rejected_rewards = self.beta * rejected_logratios.detach() agg_chosen_rewards = self.accelerator.gather(chosen_rewards) agg_rejected_rewards = self.accelerator.gather(rejected_rewards) self._metrics[mode]["rewards/chosen"].append(agg_chosen_rewards.mean().item()) self._metrics[mode]["rewards/rejected"].append(agg_rejected_rewards.mean().item()) # Reward accuracy reward_accuracies = (chosen_rewards > rejected_rewards).float() agg_reward_accuracies = self.accelerator.gather(reward_accuracies) self._metrics[mode]["rewards/accuracies"].append(agg_reward_accuracies.mean().item()) # Reward margins margins = chosen_rewards - rejected_rewards agg_margins = self.accelerator.gather(margins) self._metrics[mode]["rewards/margins"].append(agg_margins.mean().item()) # Average log probabilities for chosen and rejected completions self._metrics[mode]["logps/chosen"].append(self.accelerator.gather(chosen_logps).mean().item()) self._metrics[mode]["logps/rejected"].append(self.accelerator.gather(rejected_logps).mean().item()) return (loss, outputs) if return_outputs else loss def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): if self.use_liger_kernel: return self._compute_loss_liger(model, inputs, return_outputs) else: return self._compute_loss(model, inputs, return_outputs) # Override training step to add activation offloading context. def training_step(self, *args, **kwargs): with self.maybe_activation_offload_context: return super().training_step(*args, **kwargs) def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() # During eval, Trainer calls prediction_step. If no labels are present in the inputs, it only runs forward and # returns logits. We override prediction_step to force compute_loss, because this trainer doesn't involve labels. def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys: list[str] | None = None): inputs = self._prepare_inputs(inputs) with torch.no_grad(), self.compute_loss_context_manager(): if prediction_loss_only: loss = self.compute_loss(model, inputs, return_outputs=False) # logits aren't materialized with liger logits, labels = None, None else: loss, outputs = self.compute_loss(model, inputs, return_outputs=True) logits, labels = outputs.logits, inputs["input_ids"] return loss, logits, labels # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/trainer/grpo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from .base_config import _BaseConfig @dataclass class GRPOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`GRPOTrainer`]. This class includes only the parameters that are specific to GRPO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: > Parameters that control the model and reference model model_init_kwargs (`str`, `dict[str, Any]`, *optional*): Keyword arguments for [`~transformers.AutoModelForCausalLM.from_pretrained`], used when the `model` argument of the [`GRPOTrainer`] is provided as a string. disable_dropout (`bool`, *optional*, defaults to `False`): Whether to disable dropout in the model. This is useful for training with a reference model, as it prevents the model from generating different logprobs for the same input. cast_lm_head_to_fp32 (`bool`, *optional*, defaults to `False`): Whether to cast the language modeling head of the policy and reference models to float32. As recommended by the [ScaleRL](https://huggingface.co/papers/2510.13786) recipe. This flag is only supported when the model has untied word embedding and language modeling head layers i.e. `tie_word_embeddings` in the model config is False. > Parameters that control the data preprocessing remove_unused_columns (`bool`, *optional*, defaults to `False`): Whether to only keep the column `"prompt"` in the dataset. If you use a custom reward function that requires any column other than `"prompts"` and `"completions"`, you should keep this to `False`. num_generations (`int`, *optional*, defaults to `8`): Number of generations per prompt to sample. The effective batch size (num_processes * per_device_batch_size * gradient_accumulation_steps) must be evenly divisible by this value. num_generations_eval (`int` or `None`, *optional*): Number of generations to sample during evaluation. This allows using fewer generations during evaluation to save computation. If `None`, uses the value of `num_generations`. max_completion_length (`int` or `None`, *optional*, defaults to `256`): Maximum length of the generated completion. ds3_gather_for_generation (`bool`, *optional*, defaults to `True`): This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for generation, improving generation speed. However, disabling this option allows training models that exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option is not compatible with vLLM generation. shuffle_dataset (`bool`, *optional*, defaults to `True`): Whether to shuffle the training dataset. pad_to_multiple_of (`int`, *optional*): If set, the prompts ids and completions ids will be padded to a multiple of this value. > Parameters that control generation generation_batch_size: (`int`, *optional*): Batch size to use for generation. If `None`, it defaults to the effective training batch size: `per_device_train_batch_size * num_processes * steps_per_generation`. In other words, there is one generation batch processed per optimization step. Mutually exclusive with `steps_per_generation`. steps_per_generation: (`int`, *optional*): Number of steps per generation. If `None`, it defaults to `gradient_accumulation_steps`. Mutually exclusive with `generation_batch_size`. temperature (`float`, defaults to `1.0`): Temperature for sampling. The higher the temperature, the more random the completions. top_p (`float`, *optional*, defaults to `1.0`): Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to `1.0` to consider all tokens. top_k (`int`, *optional*, defaults to `0`): Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, top-k-filtering is disabled and all tokens are considered. min_p (`float`, *optional*): Minimum token probability, which will be scaled by the probability of the most likely token. It must be a value between `0.0` and `1.0`. Typical values are in the `0.01-0.2` range. generation_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to [`~transformers.GenerationConfig`] (if using transformers) or `SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them. chat_template_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to the `apply_chat_template` function when generating completions. repetition_penalty (`float`, *optional*, defaults to `1.0`): Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > `1.0` encourage the model to use new tokens, while values < `1.0` encourage the model to repeat tokens. use_transformers_paged (`bool`, *optional*, defaults to `False`): Whether to use the `transformers` paged implementation for generation. If set to `True`, the `transformers` paged implementation will be used for generation instead of the default padded implementation. This parameter is only effective when `use_vllm` is set to `False`. cache_implementation (`str`, *optional*): Implementation of the cache method for faster generation when `use_vllm` is set to `False`. > Parameters that control generation acceleration powered by vLLM use_vllm (`bool`, *optional*, defaults to `False`): Whether to use vLLM for generating completions. If set to `True`, the trainer will use vLLM for generation instead of the default model.generate(). Requires `vllm` to be installed. vllm_mode (`str`, *optional*, defaults to `"colocate"`): Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `"server"` or `"colocate"`. - `"server"`: The trainer will send generation requests to a separate vLLM server. Make sure a TRL vLLM server is running (start with `trl vllm-serve`). - `"colocate"`: vLLM will run in the same process and share the training GPUs. This avoids the need for a separate server but may cause resource contention with training. vllm_model_impl (`str`, *optional*, defaults to `"vllm"`): Model implementation to use for vLLM. Must be one of `"transformers"` or `"vllm"`. `"transformers"`: Use the `transformers` backend for model implementation. `"vllm"`: Use the `vllm` library for model implementation. vllm_structured_outputs_regex (`str`, *optional*): Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled. > Parameters that control the vLLM server (only used when `vllm_mode` is `"server"`) vllm_server_base_url (`str`, *optional*): Base URL for the vLLM server (e.g., `"http://localhost:8000"`). If provided, `vllm_server_host` and `vllm_server_port` are ignored. vllm_server_host (`str`, *optional*, defaults to `"0.0.0.0"`): Host of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_port (`int`, *optional*, defaults to `8000`): Port of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_timeout (`float`, *optional*, defaults to `240.0`): Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up after the timeout, a `ConnectionError` is raised. vllm_group_port (`int`, *optional*, defaults to `51216`): Port number for the weight update group. This is used to communicate with the vLLM server. Unless the port is occupied, there is no need to change it. > Parameters that control colocated vLLM execution (only used when `vllm_mode` is `"colocate"`) vllm_gpu_memory_utilization (`float`, *optional*, defaults to `0.3`): Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_gpu_memory_utilization` flag. vllm_max_model_length (`int`, *optional*): Context window for vLLM. Set it to at least the maximum prompt length in the dataset plus `max_completion_length`; if omitted, it is inferred from the model config. vllm_tensor_parallel_size (`int`, *optional*, defaults to `1`): Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_tensor_parallel_size` flag. vllm_enable_sleep_mode (`bool`, *optional*, defaults to `False`): Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory usage low, but waking the engine adds host–device transfer latency. > Parameters that control the training beta (`float`, *optional*, defaults to `0.0`): KL coefficient. If `0.0` (default), the reference model is not loaded, reducing memory usage and improving training speed. [DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement learning](https://huggingface.co/papers/2501.12948) use a value of `0.001`. num_iterations (`int`, *optional*, defaults to `1`): Number of iterations per batch (denoted as μ in the algorithm). epsilon (`float`, *optional*, defaults to `0.2`): Epsilon value for clipping. delta (`float`, *optional*): Enables the upper clipping bound in two-sided GRPO loss when set to a float. If `None` (default), standard GRPO clipping is used. Recommended to be greater than `1 + ε` when enabled. This method is introduced in the [INTELLECT-2 tech report](https://huggingface.co/papers/2505.07291). epsilon_high (`float`, *optional*): Upper-bound epsilon value for clipping. If not specified, it defaults to the same value as the lower-bound specified in argument `epsilon`. Paper [DAPO](https://huggingface.co/papers/2503.14476) recommends `0.28`. When used with `loss_type='cispo'`, this corresponds to the ε_max param specified in the [ScaleRL paper](https://huggingface.co/papers/2510.13786) and the recommended value is `5.0`. sapo_temperature_neg (`float`, *optional*, defaults to `1.05`): Temperature for tokens with non-positive advantage scores used in the `sapo` loss function. This parameter is introduced in the [Soft Adaptive Policy Optimization paper](https://huggingface.co/papers/2511.20347). sapo_temperature_pos (`float`, *optional*, defaults to `1.0`): Temperature for tokens with positive advantage scores used in the `sapo` loss function. This parameter is introduced in the [Soft Adaptive Policy Optimization paper](https://huggingface.co/papers/2511.20347). vespo_k_pos (`float`, *optional*, defaults to `2.0`): k parameter for positive advantages, it is the power exponent in the VESPO loss. Controls how aggressively we down-weight samples with low importance weights (when the importance sampling ratio < 1). vespo_lambda_pos (`float`, *optional*, defaults to `3.0`): lambda parameter for positive advantages, it is the decay factor in the VESPO loss. Controls how aggressively we down-weight samples with high importance weights (when the importance sampling ratio > 1). vespo_k_neg (`float`, *optional*, defaults to `3.0`): k parameter for negative advantages, it is the power exponent in the VESPO loss. Controls how aggressively we down-weight samples with low importance weights (when the importance sampling ratio < 1). vespo_lambda_neg (`float`, *optional*, defaults to `2.0`): lambda parameter for negative advantages, it is the exponential decay factor in the VESPO loss. Controls how aggressively we down-weight samples with high importance weights (when the importance sampling ratio > 1). importance_sampling_level (`str`, *optional*, defaults to `"token"`): Controls whether importance sampling ratios are computed at the `"token"` or `"sequence"` level. `"token"` keeps the raw per-token log-probability ratios (one weight per token). `"sequence"` averages the log-probability ratios across valid tokens to produce a single ratio per sequence. The [GSPO paper](https://huggingface.co/papers/2507.18071) shows that sequence-level sampling often yields more stable training and better alignment with sequence-level rewards. reward_weights (`list[float]`, *optional*): Weights for each reward function. Must match the number of reward functions. If `None`, all rewards are weighted equally with weight `1.0`. multi_objective_aggregation (`str`, *optional*, defaults to `"sum_then_normalize"`): Method to aggregate multiple reward functions. Supported values are: - `"sum_then_normalize"` (default): First sums the weighted rewards from each reward function, then applies reward scaling/normalization as specified by `scale_rewards` (see `scale_rewards` for details). - `"normalize_then_sum"`: First normalizes/scales each reward function across generations (within each group), then sums the normalized rewards using the specified weights. The aggregated reward is then normalized at the batch level when forming advantages. This is the suggested approach from the paper [GDPO: Group reward-Decoupled Normalization Policy Optimization for Multi-reward RL Optimization](https://huggingface.co/papers/2601.05242). scale_rewards (`str` or `bool`, *optional*, defaults to `"group"`): Specifies the scaling strategy for rewards. Supported values are: - `True` or `"group"` (default): rewards are scaled by the standard deviation within each group, ensuring unit variance within a group. - `"batch"`: rewards are scaled by the standard deviation across the entire batch, as recommended in the [PPO Lite paper](https://huggingface.co/papers/2508.08221). - `False` or `"none"`: no scaling is applied. The [Dr. GRPO paper](https://huggingface.co/papers/2503.20783) recommends not scaling rewards, as scaling by the standard deviation introduces a question-level difficulty bias. loss_type (`str`, *optional*, defaults to `"dapo"`): Specifies the loss formulation to use. Supported values are: - `"grpo"`: Aggregates token-level losses by normalizing over sequence length. Not recommended due to length bias—this approach tends to prefer shorter completions with positive advantages and longer ones with negative advantages. - `"dr_grpo"`: Aggregates token-level losses by normalizing with a global constant. This method was introduced in the [Dr. GRPO paper](https://huggingface.co/papers/2503.20783) to eliminate length bias. The value of the constant corresponds to `max_completion_length`. - `"dapo"` (default): Aggregates token-level losses by normalizing with the number of active token in the global accumulated batch. This method was introduced in the [DAPO paper](https://huggingface.co/papers/2503.14476) to eliminate length bias. - `"bnpo"`: Aggregates token-level losses by normalizing with the number of active token in the local batch. Note that normalization is performed over the local batch only, so results may slightly vary depending on the local batch size, despite a constant effective batch size. When using `per_device_train_batch_size==1`, the loss is equivalent to the GRPO loss. - `"cispo"`: Clips the importance sampling weights instead of the advantage scaled importance weights. The clipped weights are then multiplied with the advantages and policy model's log probs. Individual token losses are aggregated by normalizing with the number of active tokens in the global accumulated batch. This method was introduced in the [MiniMax-M1 paper](https://huggingface.co/papers/2506.13585). - `"sapo"`: Soft Adaptive Policy Optimization loss, as introduced in the [Soft Adaptive Policy Optimization paper](https://huggingface.co/papers/2511.20347). Replaces hard clipping with a smooth, temperature-controlled gate that adaptively attenuates off-policy updates while preserving useful learning signals. - `"luspo"`: Length-Unbiased Sequence Policy Optimization loss. A sequence-level loss that scales each sequence's loss by its length. This is a modification of GSPO and requires `importance_sampling_level="sequence"`. Introduced in the [LUSPO paper](https://huggingface.co/papers/2602.05261). - `"vespo"`: Variational Sequence-Level Soft Policy Optimization. Replaces hard clipping with a smooth, asymmetric Gamma weighting function applied directly to sequence-level importance weights. Introduced in the [VESPO paper](https://huggingface.co/papers/2602.10693). mask_truncated_completions (`bool`, *optional*, defaults to `False`): When enabled, truncated completions are excluded from the loss calculation, preventing them from being incorrectly penalized and introducing noise during training. According to the [DAPO](https://huggingface.co/papers/2503.14476) paper, this is a good practice for training stability. sync_ref_model (`bool`, *optional*, defaults to `False`): Whether to synchronize the reference model with the active model every `ref_model_sync_steps` steps, using the `ref_model_mixup_alpha` parameter. This synchronization originates from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper. ref_model_mixup_alpha (`float`, *optional*, defaults to `0.6`): α parameter from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper, which controls the mix between the current policy and the previous reference policy during updates. The reference policy is updated according to the equation: `π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`. ref_model_sync_steps (`int`, *optional*, defaults to `512`): τ parameter from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper, which determines how frequently the current policy is synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`. top_entropy_quantile (`float`, *optional*, defaults to `1.0`): ρ parameter from [Beyond the 80/20 Rule](https://huggingface.co/papers/2506.01939). Keeps in the policy loss term only the top-ρ quantile of tokens by entropy of the probability distribution at each sequence position, improving results. Range: `[0.0-1.0]`. A value of `0.0` masks all but the highest entropy token; `1.0` keeps all tokens. The paper recommends a value of `0.2`. If used with `mask_truncated_completions=True`, only tokens from non-truncated completions are considered. max_tool_calling_iterations (`int`, *optional*): Maximum number of tool-calling turns when training an agent. If `None`, there is no limit and generation stops when the model generates a response turn with no tool calls or when the total response length reaches `max_model_length`. vllm_importance_sampling_correction (`bool`, *optional*, defaults to `True`): Whether to apply Importance Sampling (IS) to correct for the mismatch between vLLM completion logprobs and recomputed training logprobs. If set to `False`, no IS is applied regardless of `vllm_importance_sampling_mode`. When `True`, the selected mode determines how the IS ratios are computed and constrained. vllm_importance_sampling_mode (`str`, *optional*, defaults to `"sequence_mask"`): Specifies how Importance Sampling is performed when `vllm_importance_sampling_correction=True`. Possible values are: - `"token_truncate"`: Token-level truncated IS (default). Per-token ratios are clipped from above at C. - `"token_mask"`: Token-level masked IS. Per-token ratios above C are set to zero. - `"sequence_truncate"`: Sequence-level truncated IS. A single sequence ratio is clipped from above at C and applied to all tokens in the sequence. - `"sequence_mask"`: Sequence-level masked IS. Sequences with ratios above C are masked out. vllm_importance_sampling_cap (`float`, *optional*, defaults to `3.0`): Importance sampling cap C used by `vllm_importance_sampling_mode`. For `*_truncate` modes, importance ratios are clipped from above at C. For `*_mask` modes, ratios larger than C are set to zero. off_policy_mask_threshold (`float`, *optional*): Threshold for off-policy sequence masking. If `None`, off-policy sequence masking is disabled. When set, sequences with negative advantages and high KL divergence are masked out to stabilize training. This parameter corresponds to the `delta` threshold in Equation 9 of the [DeepSeek-V3.2 paper](https://huggingface.co/papers/2512.02556). It expects a positive value (e.g., 0.5). use_bias_correction_kl (`bool`, *optional*, defaults to `False`): Whether to use the unbiased KL divergence estimator with importance sampling correction. This corrects the KL divergence estimate by multiplying it with the importance sampling ratio. This is described in the [DeepSeek-V3.2 paper](https://huggingface.co/papers/2512.02556). > Parameters that control the logging log_completions (`bool`, *optional*, defaults to `False`): Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is installed, it prints the sample. If `wandb` and/or `trackio` logging is enabled, it logs it to `wandb` and/or `trackio`. num_completions_to_print (`int`, *optional*): Number of completions to print with `rich`. If `None`, all completions are logged. log_unique_prompts (`bool`, *optional*, defaults to `False`): Whether to log unique prompts. If `True`, only unique prompts are logged. If `False`, all prompts are logged. log_completions_hub_repo (`str`, *optional*): Hugging Face Hub repository to save the completions. Should be a complete repository name like `'username/reponame'` or `'orgname/reponame'`, or just `'reponame'` in which case the repository will be created in the currently-logged-in Hugging Face user's namespace. Note that this repository will be public unless you set `hub_private_repo=True` or your organization's default is to create private repositories." > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) # Parameters that control the model and reference model model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments for `transformers.AutoModelForCausalLM.from_pretrained`, used when the `model` " "argument of the `GRPOTrainer` is provided as a string." }, ) disable_dropout: bool = field( default=False, metadata={ "help": "Whether to disable dropout in the model. This is useful for training with a reference model, as " "it prevents the model from generating different logprobs for the same input." }, ) cast_lm_head_to_fp32: bool = field( default=False, metadata={ "help": "Whether to cast the language modeling head of the policy and reference, models to float32." "As recommended by the [ScaleRL](https://huggingface.co/papers/2510.13786) recipe. This flag is only " "supported when the model has untied word embedding and language modeling head layers i.e. " "`tie_word_embeddings` in the model config is False." }, ) # Parameters that control the data preprocessing # The default value remove_unused_columns is overwritten from the parent class, because in GRPO we usually rely on # additional columns to compute the reward remove_unused_columns: bool | None = field( default=False, metadata={ "help": "Whether to only keep the column 'prompt' in the dataset. If you use a custom reward function " "that requires any column other than 'prompts' and 'completions', you should keep this to `False`." }, ) num_generations: int | None = field( default=8, metadata={ "help": "Number of generations to sample. The effective batch size (num_processes * per_device_batch_size " "* gradient_accumulation_steps) must be evenly divisible by this value." }, ) num_generations_eval: int | None = field( default=None, metadata={ "help": "Number of generations to sample during evaluation. This allows using fewer generations during " "evaluation to save computation. If `None`, uses the value of `num_generations`." }, ) max_completion_length: int | None = field( default=256, metadata={"help": "Maximum length of the generated completion."}, ) ds3_gather_for_generation: bool = field( default=True, metadata={ "help": "This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for " "generation, improving generation speed. However, disabling this option allows training models that " "exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option " "is not compatible with vLLM generation." }, ) shuffle_dataset: bool | None = field( default=True, metadata={"help": "Whether to shuffle the training dataset."}, ) pad_to_multiple_of: int | None = field( default=None, metadata={"help": "If set, the prompts ids and completions ids will be padded to a multiple of this value."}, ) # Parameters that control generation generation_batch_size: int | None = field( default=None, metadata={ "help": "Batch size to use for generation. If `None`, it defaults to the effective training batch size: " "`per_device_train_batch_size * num_processes * steps_per_generation`." }, ) steps_per_generation: int | None = field( default=None, metadata={"help": "Number of steps per generation. If `None`, it defaults to `gradient_accumulation_steps`."}, ) temperature: float = field( default=1.0, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) top_p: float = field( default=1.0, metadata={ "help": "Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. " "Set to 1.0 to consider all tokens." }, ) top_k: int = field( default=0, metadata={ "help": "Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, " "top-k-filtering is disabled and all tokens are considered." }, ) min_p: float | None = field( default=None, metadata={ "help": "Minimum token probability, which will be scaled by the probability of the most likely token. It " "must be a value between 0.0 and 1.0. Typical values are in the 0.01-0.2 range." }, ) generation_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to `GenerationConfig` (if using transformers) or " "`SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the " "generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that " "conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them." }, ) chat_template_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to the `apply_chat_template` function when generating " "completions." }, ) repetition_penalty: float = field( default=1.0, metadata={ "help": "Float that penalizes new tokens based on whether they appear in the prompt and the generated " "text so far. Values > 1.0 encourage the model to use new tokens, while values < 1.0 encourage the model " "to repeat tokens." }, ) use_transformers_paged: bool = field( default=False, metadata={ "help": "Whether to use the `transformers` paged implementation for generation. If set to `True`, the " "`transformers` paged implementation will be used for generation instead of the default padded " "implementation. This parameter is only effective when `use_vllm` is set to `False`." }, ) cache_implementation: str | None = field( default=None, metadata={"help": "Implementation of the cache method for faster generation when use_vllm is set to False."}, ) # Parameters that control generation acceleration powered by vLLM use_vllm: bool = field( default=False, metadata={ "help": "Whether to use vLLM for generating completions. If set to `True`, the trainer will use vLLM for " "generation instead of the default model.generate(). Requires `vllm` to be installed." }, ) vllm_mode: str = field( default="colocate", metadata={ "help": "Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `'server'` or " "`'colocate'`. `'server'`: The trainer will send generation requests to a separate vLLM server. Make sure " "a TRL vLLM server is running (start with `trl vllm-serve`). `'colocate'`: vLLM will run in the same " "process and share the training GPUs. This avoids the need for a separate server but may cause resource " "contention with training." }, ) vllm_model_impl: str = field( default="vllm", metadata={ "help": "Model implementation to use for vLLM. Must be one of `transformers` or `vllm`. `transformers`: " "Use the `transformers` backend for model implementation. `vllm`: Use the `vllm` library for " "model implementation." }, ) vllm_enable_sleep_mode: bool = field( default=False, metadata={ "help": "Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory " "usage low, but waking the engine adds host–device transfer latency." }, ) vllm_structured_outputs_regex: str | None = field( default=None, metadata={"help": "Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled."}, ) # Parameters that control the vLLM server (only used when `vllm_mode` is `"server"`) vllm_server_base_url: str | None = field( default=None, metadata={ "help": "Base URL for the vLLM server (e.g., 'http://localhost:8000'). If provided, `vllm_server_host` " "and `vllm_server_port` are ignored." }, ) vllm_server_host: str = field( default="0.0.0.0", metadata={"help": "Host of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_port: int = field( default=8000, metadata={"help": "Port of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_timeout: float = field( default=240.0, metadata={ "help": "Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up " "after the timeout, a `ConnectionError` is raised." }, ) vllm_group_port: int = field( default=51216, metadata={ "help": "Port number for the weight update group. This is used to communicate with the vLLM server. " "Unless the port is occupied, there is no need to change it.", }, ) # Parameters that control colocated vLLM execution (only used when `vllm_mode` is `"colocate"`) vllm_gpu_memory_utilization: float = field( default=0.3, metadata={ "help": "Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_gpu_memory_utilization` flag." }, ) vllm_max_model_length: int | None = field( default=None, metadata={ "help": "Context window for vLLM. Set it to at least the maximum prompt length in the dataset plus " "`max_completion_length`; if omitted, it is inferred from the model config." }, ) vllm_tensor_parallel_size: int = field( default=1, metadata={ "help": "Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_tensor_parallel_size` flag." }, ) # Parameters that control the training beta: float = field( default=0.0, metadata={ "help": "KL coefficient. If `0.0` (default), the reference model is not loaded, reducing memory usage and " "improving training speed. [DeepSeek-R1 incentivizes reasoning in LLMs through reinforcement " "learning](https://huggingface.co/papers/2501.12948) use a value of `0.001`." }, ) num_iterations: int = field( default=1, metadata={"help": "Number of iterations per batch (denoted as μ in the algorithm)."}, ) epsilon: float = field( default=0.2, metadata={"help": "Epsilon value for clipping."}, ) delta: float | None = field( default=None, metadata={ "help": "Enables the upper clipping bound in two-sided GRPO loss when set to a float. If `None` " "(default), standard GRPO clipping is used. Recommended to be greater than `1 + ε` when enabled. This " "method is introduced in the [INTELLECT-2 tech report](https://huggingface.co/papers/2505.07291)." }, ) epsilon_high: float | None = field( default=None, metadata={ "help": "Upper-bound epsilon value for clipping. If not specified, it defaults to the same value as the " "lower-bound specified in argument `epsilon`. Paper DAPO recommends `0.28`. " "When used with `loss_type='cispo'`, this corresponds to the ε_max param specified in the" "[ScaleRL paper]https://huggingface.co/papers/2510.13786) and the recommended value is `5.0`." }, ) sapo_temperature_neg: float = field( default=1.05, metadata={ "help": "Temperature for tokens with non-positive advantage scores used in the `sapo` loss function. " "This parameter is introduced in the [Soft Adaptive Policy Optimization " "paper](https://huggingface.co/papers/2511.20347)." }, ) sapo_temperature_pos: float = field( default=1.0, metadata={ "help": "Temperature for tokens with positive advantage scores used in the `sapo` loss function. " "This parameter is introduced in the [Soft Adaptive Policy Optimization " "paper](https://huggingface.co/papers/2511.20347)." }, ) vespo_k_pos: float = field( default=2.0, metadata={ "help": "k parameter for positive advantages, it is the power exponent in the VESPO loss. Controls how " "aggressively we down-weight samples with low importance weights (when the importance sampling ratio < 1)." }, ) vespo_lambda_pos: float = field( default=3.0, metadata={ "help": "lambda parameter for positive advantages, it is the decay factor in the VESPO loss. Controls " "how aggressively we down-weight samples with high importance weights (when the importance sampling ratio " "> 1)." }, ) vespo_k_neg: float = field( default=3.0, metadata={ "help": "k parameter for negative advantages, it is the power exponent in the VESPO loss. Controls how " "aggressively we down-weight samples with low importance weights (when the importance sampling ratio < 1)." }, ) vespo_lambda_neg: float = field( default=2.0, metadata={ "help": "lambda parameter for negative advantages, it is the exponential decay factor in the VESPO loss. " "Controls how aggressively we down-weight samples with high importance weights (when the importance " "sampling ratio > 1)." }, ) importance_sampling_level: str = field( default="token", metadata={ "help": "Controls whether importance sampling ratios are computed at the `'token'` or `'sequence'` level. " "`'token'` keeps the raw per-token log-probability ratios (one weight per token). `'sequence'` averages " "the log-probability ratios across valid tokens to produce a single ratio per sequence. The GSPO paper " "shows that sequence-level sampling often yields more stable training and better alignment with " "sequence-level rewards." }, ) reward_weights: list[float] | None = field( default=None, metadata={ "help": "Weights for each reward function. Must match the number of reward functions. If `None`, all " "rewards are weighted equally with weight `1.0`." }, ) multi_objective_aggregation: str = field( default="sum_then_normalize", metadata={ "help": "Method to aggregate multiple reward functions. Supported values are: " "`'sum_then_normalize'` (default): First sums the weighted rewards from each reward function, then " "applies reward scaling/normalization as specified by `scale_rewards` (see `scale_rewards` for details). " "`'normalize_then_sum'`: First normalizes/scales each reward function across generations (within each " "group), then sums the normalized rewards using the specified weights. The aggregated reward is then " "normalized at the batch level when forming advantages. This is the suggested approach from the paper " "GDPO: Group reward-Decoupled Normalization Policy Optimization for Multi-reward RL Optimization." }, ) scale_rewards: str = field( default="group", metadata={ "help": "Specifies the scaling strategy for rewards. Supported values are: " "`True` or `group'` (default): rewards are scaled by the standard deviation within each group, ensuring " "unit variance within a group. " "`'batch'`: rewards are scaled by the standard deviation across the entire batch, as recommended in the " "PPO Lite paper. " "`False` or `'none'`: no scaling is applied. The Dr. GRPO paper recommends not scaling rewards, as " "scaling by the standard deviation introduces a question-level difficulty bias." }, ) loss_type: str = field( default="dapo", metadata={ "help": "Specifies the loss formulation to use. Supported values are 'grpo', 'dapo', 'bnpo', and " "'dr_grpo'. " "'grpo': Aggregates token-level losses by normalizing over sequence length. Not recommended due to length " "bias—this approach tends to prefer shorter completions with positive advantages and longer ones with " "negative advantages. " "'dapo' (default): Aggregates token-level losses by normalizing with the number of active token in the " "global accumulated batch. This method was introduced in the DAPO paper to eliminate length bias. " "'dr_grpo': Aggregates token-level losses by normalizing with a global constant. This method was " "introduced in the Dr. GRPO paper to eliminate length bias. The value of the constant corresponds to " "`max_completion_length`. " "'bnpo': Aggregates token-level losses by normalizing with the number of active token in the local batch. " "Note that normalization is performed over the local batch only, so results may slightly vary depending " "on the local batch size, despite a constant effective batch size. When using " "`per_device_train_batch_size==1`, the loss is equivalent to the GRPO loss." "'cispo': Clips the importance sampling weights instead of the advantage scaled importance weights. " "The clipped weights are then multiplied with the advantages and policy model's log probs. " "Individual token losses are aggregated by normalizing with the number of active tokens in " "the global accumulated batch. This method was introduced in the " "[MiniMax-M1 paper](https://huggingface.co/papers/2506.13585). " "'sapo': Soft Adaptive Policy Optimization loss, as introduced in the " "[Soft Adaptive Policy Optimization paper](https://huggingface.co/papers/2511.20347). " "Replaces hard clipping with a smooth, temperature-controlled gate that adaptively attenuates " "off-policy updates while preserving useful learning signals." "'luspo': Length-Unbiased Sequence Policy Optimization loss. A sequence-level loss that scales each " "sequence's loss by its length. This is a modification of GSPO and requires " "`importance_sampling_level='sequence'`. Introduced in the [LUSPO " "paper](https://huggingface.co/papers/2602.05261)." "'vespo': Variational Sequence-Level Soft Policy Optimization. Replaces hard clipping with a smooth, " "asymmetric Gamma weighting function applied directly to sequence-level importance weights. Introduced in " "the [VESPO paper](https://huggingface.co/papers/2602.10693)." }, ) mask_truncated_completions: bool = field( default=False, metadata={ "help": "When enabled, truncated completions are excluded from the loss calculation, preventing them from " "being incorrectly penalized and introducing noise during training. According to the DAPO paper, this is " "a good practice for training stability." }, ) sync_ref_model: bool = field( default=False, metadata={ "help": "Whether to synchronize the reference model with the active model every `ref_model_sync_steps` " "steps, using the `ref_model_mixup_alpha` parameter." }, ) ref_model_mixup_alpha: float = field( default=0.6, metadata={ "help": "α parameter from the TR-DPO paper, which controls the mix between the current policy and the " "previous reference policy during updates. The reference policy is updated according to the equation: " "`π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`." }, ) ref_model_sync_steps: int = field( default=512, metadata={ "help": "τ parameter from the TR-DPO paper, which determines how frequently the current policy is " "synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`." }, ) top_entropy_quantile: float = field( default=1.0, metadata={ "help": "ρ parameter from Beyond the 80/20 Rule. Keeps in the policy loss term only the top-ρ quantile of " "tokens by entropy of the probability distribution at each sequence position, improving results. Range: " "[0.0-1.0]. A value of `0.0` masks all but the highest entropy token; `1.0` keeps all tokens. The paper " "recommends a value of `0.2`. If used with `mask_truncated_completions=True`, only tokens from " "non-truncated completions are considered." }, ) max_tool_calling_iterations: int | None = field( default=None, metadata={ "help": "Maximum number of tool-calling turns when training an agent. If `None`, there is no limit and " "generation stops when the model generates a response turn with no tool calls or when the total " "response length reaches `max_model_length`." }, ) vllm_importance_sampling_correction: bool = field( default=True, metadata={ "help": "Whether to apply Importance Sampling (IS) to correct for the mismatch between vLLM " "completion logprobs and recomputed training logprobs. If set to `False`, no IS is applied " "regardless of `vllm_importance_sampling_mode`. When `True`, the selected mode determines how " "IS ratios are computed and constrained." }, ) vllm_importance_sampling_mode: str = field( default="sequence_mask", metadata={ "help": "Specifies how Importance Sampling (IS) is performed when " "vllm_importance_sampling_correction=True. Modes are defined along two orthogonal " "dimensions: (1) constraint, which determines how to handle ratios above " "vllm_importance_sampling_cap (C)—either truncation (clip from above, ρ ← min(ρ, C)) or " "masking (set ratios above C to zero); and (2) granularity, which determines whether " "ratios are computed per token or as a single sequence-level ratio applied to all tokens. " "Supported options are: 'token_truncate', 'token_mask', 'sequence_truncate', and " "'sequence_mask'." }, ) vllm_importance_sampling_cap: float = field( default=3.0, metadata={ "help": "Importance sampling cap C used by `vllm_importance_sampling_mode`. For '*_truncate' modes, " "ratios are clipped from above at C. For '*_mask' modes, ratios larger than C are set to zero." }, ) off_policy_mask_threshold: float | None = field( default=None, metadata={ "help": "Threshold for off-policy sequence masking. If `None`, off-policy sequence masking is disabled. " "When set, sequences with negative advantages and high KL divergence are masked out to stabilize " "training. This parameter corresponds to the `delta` threshold in Equation 9 of the [DeepSeek-V3.2 " "paper](https://huggingface.co/papers/2512.02556). It expects a positive value (e.g., 0.5)." }, ) use_bias_correction_kl: bool = field( default=False, metadata={ "help": "Whether to use the unbiased KL divergence estimator with importance sampling correction. This " "corrects the KL divergence estimate by multiplying it with the importance sampling ratio. " "This is described in the [DeepSeek-V3.2 paper](https://huggingface.co/papers/2512.02556)." }, ) # Parameters that control the logging log_completions: bool = field( default=False, metadata={ "help": "Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is " "installed, it prints the sample. If `wandb` logging is enabled, it logs it to `wandb`." }, ) num_completions_to_print: int | None = field( default=None, metadata={"help": "Number of completions to print with `rich`. If `None`, all completions are logged."}, ) log_unique_prompts: bool = field( default=False, metadata={ "help": "Whether to log unique prompts. If `True`, only unique prompts are logged. If `False`, all " "prompts are logged." }, ) log_completions_hub_repo: str | None = field( default=None, metadata={ "help": "Hugging Face Hub repository to save the completions. Should be a complete repository name like " "`'username/reponame'` or `'orgname/reponame'`, or just `'reponame'` in which case the repository will " "be created in the currently-logged-in Hugging Face user's namespace. Note that this repository will be " "public unless you set `hub_private_repo=True` or your organization's default is to create private " "repositories." }, ) def __post_init__(self): super().__post_init__() self.scale_rewards = {True: "group", False: "none"}.get(self.scale_rewards, self.scale_rewards) if self.log_completions_hub_repo is not None and not self.log_completions: raise ValueError( "log_completions_hub_repo is set, but log_completions is False. Enable log_completions to upload " "completions to the Hub, or unset log_completions_hub_repo." ) num_processes = self.world_size # The current default effective batch size if self.generation_batch_size is None and self.steps_per_generation is None: self.steps_per_generation = self.gradient_accumulation_steps self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation elif self.generation_batch_size is not None and self.steps_per_generation is None: # Just ensure the value is divisible by the global batch size if self.generation_batch_size % (self.per_device_train_batch_size * num_processes) != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by the global batch size " f"({self.per_device_train_batch_size * num_processes})." ) self.steps_per_generation = self.generation_batch_size // ( self.per_device_train_batch_size * num_processes ) elif self.generation_batch_size is None and self.steps_per_generation is not None: self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation else: raise ValueError( "'generation_batch_size' and 'steps_per_generation' can not be both configured at the same time" ) if self.do_eval and self.eval_strategy != "no": # Determine the number of generations to use for evaluation num_generations = self.num_generations_eval or self.num_generations # Just ensure the value is divisible by the global batch size if (self.per_device_eval_batch_size * num_processes) % num_generations != 0: raise ValueError( f"The global eval batch size ({self.per_device_eval_batch_size} * {num_processes}) must be " f"divisible by the number of generations used for evaluation ({num_generations})." ) # The generation batch must contain full prompt groups (no partials), so it must be divisible by # num_generations. if self.generation_batch_size % self.num_generations != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by num_generations " f"({self.num_generations})." ) if self.num_generations < 2: raise ValueError( "GRPO requires at least 2 generations per prompt to calculate the advantages. You provided " f"{self.num_generations}, which is less than the minimum required." ) if self.delta is not None and self.use_liger_kernel: raise ValueError("Liger kernel does not support two-sided GRPO loss yet.") ================================================ FILE: trl/trainer/grpo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import atexit import copy import importlib.resources as pkg_resources import inspect import math import os import sys import textwrap import time import warnings from collections import defaultdict, deque from collections.abc import Callable from contextlib import nullcontext from pathlib import Path from typing import Any, Protocol import numpy as np import pandas as pd import torch import torch.utils.data import transformers from accelerate.logging import get_logger from accelerate.utils import gather, gather_object, is_peft_model, set_seed from datasets import Dataset, IterableDataset from huggingface_hub import CommitScheduler, DatasetCard, DatasetCardData, create_repo from packaging.version import Version from torch import nn from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.utils.data import Sampler from transformers import ( AutoModelForSequenceClassification, AutoProcessor, AutoTokenizer, GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, is_trackio_available, is_wandb_available, ) from transformers.utils import is_peft_available, is_rich_available from ..chat_template_utils import add_response_schema, get_training_chat_template, parse_response from ..data_utils import ( apply_chat_template, is_conversational, prepare_multimodal_messages, ) from ..extras.profiling import profiling_context, profiling_decorator from ..generation.vllm_generation import VLLMGeneration from ..import_utils import is_jmespath_available, is_liger_kernel_available from ..models import prepare_deepspeed, prepare_fsdp, unwrap_model_for_generation from ..models.utils import _ForwardRedirection, disable_gradient_checkpointing from .base_trainer import _BaseTrainer from .callbacks import SyncRefModelCallback from .grpo_config import GRPOConfig from .utils import ( RepeatSampler, create_model_from_path, disable_dropout_in_model, entropy_from_logits, get_config_model_id, identity, nanmax, nanmin, nanstd, pad, print_prompt_completions_sample, selective_log_softmax, shuffle_sequence_dict, shutdown_event_loop_in_daemon, split_pixel_values_by_grid, split_tensor_dict, start_event_loop_in_daemon, unsplit_pixel_values_by_grid, use_adapter, ) if is_peft_available(): from peft import PeftConfig, PeftModel, get_peft_model if is_liger_kernel_available(): from liger_kernel.chunked_loss import LigerFusedLinearGRPOLoss if is_wandb_available(): import wandb if is_trackio_available(): import trackio logger = get_logger(__name__) # A reward function can be a string, interpreted as a model ID and loaded as a pretrained model, a pretrained model, or # a callable that returns a list of floats (the rewards). The callable receives prompts, completions, and additional # arguments from the trainer (refer to the trainer's source for details). To ensure forward compatibility, it should # accept **kwargs. RewardFunc = str | PreTrainedModel | Callable[..., list[float | None]] # What we call a rollout function is a callable that takes prompts (list) and the trainer instance as parameters and # returns a dict of generation results. Those results must include "prompt_ids", "completion_ids", and "logprobs" # fields. Any extra fields (per-completion) are forwarded to the reward functions. RolloutFunc = Callable[[list[str], "GRPOTrainer"], dict[str, Any]] class _SupportsReset(Protocol): def reset(self, **kwargs) -> str | None: ... EnvironmentFactory = Callable[[], _SupportsReset] class GRPOTrainer(_BaseTrainer): """ Trainer for the Group Relative Policy Optimization (GRPO) method. This algorithm was initially proposed in the paper [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300). Example: ```python from trl import GRPOTrainer from trl.rewards import accuracy_reward from datasets import load_dataset dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = GRPOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `.from_pretrained` (where `` is derived from the model config) with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. - A [`~peft.PeftModel`] object. Only causal language models are supported. reward_funcs (`RewardFunc | list[RewardFunc]`): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function, such as: - A string: The *model ID* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForSequenceClassification.from_pretrained`] with `num_labels=1` and the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object: Only sequence classification models are supported. - A custom reward function: The function is provided with the prompts and the generated completions, plus any additional columns in the dataset. It should return a list of rewards. Custom reward functions can be either synchronous or asynchronous and can also return `None` when the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returns `None` for a sample, that reward function is excluded from the reward calculation for that sample. For more details, see [Using a custom reward function](#using-a-custom-reward-function). The trainer's state is also passed to the reward function. The trainer's state is an instance of [`~transformers.TrainerState`] and can be accessed by accessing the `trainer_state` argument to the reward function's signature. - A list of reward functions, where each item can independently be any of the above types. Mixing different types within the list (e.g., a string model ID and a custom reward function) is allowed. args ([`GRPOConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. It must include a column `"prompt"`. Any additional columns in the dataset is ignored. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. The padding side must be set to "left". If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. reward_processing_classes ([`~transformers.PreTrainedTokenizerBase`] or `list[PreTrainedTokenizerBase]`, *optional*): Processing classes corresponding to the reward functions specified in `reward_funcs`. Can be either: - A single processing class: Used when `reward_funcs` contains only one reward function. - A list of processing classes: Must match the order and length of the reward functions in `reward_funcs`. If set to `None`, or if an element of the list corresponding to a [`~transformers.PreTrainedModel`] is `None`, the tokenizer for the model is automatically loaded using [`~transformers.AutoTokenizer.from_pretrained`]. For elements in `reward_funcs` that are custom reward functions (not [`~transformers.PreTrainedModel`]), the corresponding entries in `reward_processing_classes` are ignored. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. tools (list of `Callable`, *optional*): A list of callable tool functions (sync or async) that the model can invoke during generation. Each tool should be a standard Python function with properly type-hinted arguments and return values, and a Google-style docstring describing its purpose, arguments, and return value. For more details, see: https://huggingface.co/docs/transformers/en/chat_extras#passing-tools. The model uses the function's name, type hints, and docstring to determine how to call it. Ensure that the model's chat template supports tool use and that it has been fine-tuned for tool calling. rollout_func (`RolloutFunc`, *optional*): Function to use for generating completions. It receives the list of prompts allocated to the current process and the trainer instance. It must return a dict with `"prompt_ids"`, `"completion_ids"`, and `"logprobs"` fields, and can optionally return `"logprob_token_ids"` (same shape as `"logprobs"`). Any other fields are forwarded to the reward functions. The function receives the raw per-process prompt slice with no duplication; it is responsible for returning the correct number of completions per prompt (see `num_generations` / `num_generations_eval` on the trainer). This feature is experimental and may change or be removed at any time without prior notice. environment_factory (`EnvironmentFactory`, *optional*): A callable that creates and returns an environment instance. The environment class should define methods that can be invoked as tools during generation. Each method should comply with the same requirements as the `tools` described above. If `environment_factory` is provided, an instance of the environment is created for each generation in the batch, allowing for parallel and independent interactions. The environment must also implement a callable `reset` method that can be used to reset state between generations. The `reset` method should return either `None` or a string: when it returns a string, that string is appended to the last user message before generation. This feature is experimental and may change or be removed at any time without prior notice. """ _tag_names = ["trl", "grpo"] _name = "GRPO" _paper = { "title": "DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models", "id": "2402.03300", # docstyle-ignore "citation": textwrap.dedent("""\ @article{shao2024deepseekmath, title = {{DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}}, author = {Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo}, year = 2024, eprint = {arXiv:2402.03300}, } """), } def __init__( self, model: "str | PreTrainedModel | PeftModel", reward_funcs: RewardFunc | list[RewardFunc], args: GRPOConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), peft_config: "PeftConfig | None" = None, tools: list[Callable] | None = None, rollout_func: RolloutFunc | None = None, environment_factory: EnvironmentFactory | None = None, ): # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = GRPOConfig(f"{model_name}-GRPO") # Model if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model = create_model_from_path(model, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the `GRPOConfig`, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) # Some models (SmolVLM/Idefics3) don't support `logits_to_keep` argument and error out if we pass it # Inspect the forward method before we wrap the model with PEFT self.model_kwarg_keys = ( inspect.signature(model.forward).parameters.keys() if not hasattr(model, "get_base_model") else inspect.signature(model.get_base_model().forward).parameters.keys() ) # Processing class if processing_class is None: processing_class = AutoProcessor.from_pretrained( get_config_model_id(model.config), truncation_side="left", padding_side="left" ) # Handle pad token for processors or tokenizers if isinstance(processing_class, ProcessorMixin): tokenizer = processing_class.tokenizer elif isinstance(processing_class, PreTrainedTokenizerBase): tokenizer = processing_class else: raise TypeError("The `processing_class` must be either a `PreTrainedTokenizerBase` or a `ProcessorMixin`") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token self.pad_token = tokenizer.pad_token self.pad_token_id = tokenizer.pad_token_id self.eos_token_id = tokenizer.eos_token_id if is_peft_available() and is_peft_model(model) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge " "and unload the existing adapter, save the resulting base model, and then pass that base model along " "with the new `peft_config` to the trainer." ) if is_peft_available() and is_peft_model(model) and args.beta != 0.0: # If the model is a PEFT model with a pretrained adapter, we need to create a "ref" adapter that is a copy # of the "default" adapter, so that we can use it as the reference model during GRPO training. model.add_adapter("ref", model.peft_config["default"]) for name, param in model.named_parameters(): if ".default." in name: ref_name = name.replace(".default.", ".ref.") ref_param = model.get_parameter(ref_name) ref_param.data.copy_(param.data) # Create PEFT model if peft_config is not None: model = get_peft_model(model, peft_config) # When using gradient checkpointing with PEFT, we need to enable input gradients. transformers.Trainer normally # handles this, but a bug currently prevents it; see https://github.com/huggingface/transformers/issues/42489 if is_peft_available() and is_peft_model(model) and args.gradient_checkpointing: model.enable_input_require_grads() # When using QLoRA, the PEFT adapter weights are converted to bf16 to follow the recommendations from the # original paper (see https://huggingface.co/papers/2305.14314, paragraph 3). Normally, this can be done by # passing `autocast_adapter_dtype=False` to `get_peft_model`, but this option is not yet supported for # quantized models. See: https://github.com/huggingface/peft/issues/2889 # Non-quantized models do not have the `is_loaded_in_{8,4}bit` attributes, whereas quantized models do if getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False): for param in model.parameters(): if param.requires_grad: param.data = param.data.to(torch.bfloat16) # Reward functions if not isinstance(reward_funcs, list): reward_funcs = [reward_funcs] self.reward_func_names = [] for i, reward_func in enumerate(reward_funcs): if isinstance(reward_func, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None reward_funcs[i] = AutoModelForSequenceClassification.from_pretrained( reward_func, num_labels=1, **model_init_kwargs ) if isinstance(reward_funcs[i], nn.Module): # Use Module over PretrainedModel for compat w/ compiled models self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1]) else: self.reward_func_names.append(reward_funcs[i].__name__) self.reward_funcs = reward_funcs # Reward weights if args.reward_weights is not None: if len(args.reward_weights) != len(reward_funcs): raise ValueError( f"Number of reward weights ({len(args.reward_weights)}) must match number of reward " f"functions ({len(reward_funcs)})" ) self.reward_weights = torch.tensor(args.reward_weights, dtype=torch.float32) else: self.reward_weights = torch.ones(len(reward_funcs), dtype=torch.float32) # Reward processing class if reward_processing_classes is None: reward_processing_classes = [None] * len(reward_funcs) elif not isinstance(reward_processing_classes, list): reward_processing_classes = [reward_processing_classes] if len(reward_processing_classes) != len(reward_funcs): raise ValueError( f"The number of reward processing classes ({len(reward_processing_classes)}) must match the number of " f"reward functions ({len(reward_funcs)})." ) for i, (reward_processing_class, reward_func) in enumerate( zip(reward_processing_classes, reward_funcs, strict=True) ): if isinstance(reward_func, PreTrainedModel): if reward_processing_class is None: reward_processing_class = AutoTokenizer.from_pretrained(get_config_model_id(reward_func.config)) if reward_processing_class.pad_token_id is None: reward_processing_class.pad_token = reward_processing_class.eos_token # The reward model computes the reward for the latest non-padded token in the input sequence. # So it's important to set the pad token ID to the padding token ID of the processing class. reward_func.config.pad_token_id = reward_processing_class.pad_token_id reward_processing_classes[i] = reward_processing_class self.reward_processing_classes = reward_processing_classes # Rollout function if rollout_func is not None and os.environ.get("TRL_EXPERIMENTAL_SILENCE", "0") != "1": warnings.warn( "You are using 'rollout_func', which is an experimental feature. This API may change or be removed at " "any time without prior notice. Silence this warning by setting environment variable " "TRL_EXPERIMENTAL_SILENCE=1.", UserWarning, stacklevel=2, ) self.rollout_func = rollout_func if environment_factory is not None and os.environ.get("TRL_EXPERIMENTAL_SILENCE", "0") != "1": warnings.warn( "You are using 'environment_factory', which is an experimental feature. This API may change or be " "removed at any time without prior notice. Silence this warning by setting environment variable " "TRL_EXPERIMENTAL_SILENCE=1.", UserWarning, stacklevel=2, ) # Tools if tools: if not Version(transformers.__version__) >= Version("5.0.0"): raise ImportError( "Using tools with GRPOTrainer requires transformers version 5.0.0 or higher. Please upgrade " "transformers with `pip install --upgrade transformers` to use this feature." ) if environment_factory: if not Version(transformers.__version__) >= Version("5.2.0"): raise ImportError( "Using `environment_factory` with GRPOTrainer requires transformers version 5.2.0 or higher. " "Please install transformers from the main branch with `pip install " "git+https://github.com/huggingface/transformers.git@main` to use this feature." ) if tools or environment_factory: if not is_jmespath_available(): raise ImportError( "Using tools with GRPOTrainer requires the jmespath library for response parsing. Please install " "it with `pip install jmespath` to use this feature." ) # Create the environments and extract their methods to be used as tools. We create one environment per rollout generation_batch_size = args.per_device_train_batch_size * args.steps_per_generation if environment_factory is not None: self.environments = [environment_factory() for _ in range(generation_batch_size)] environment_methods = [[] for _ in range(generation_batch_size)] for i, environment in enumerate(self.environments): has_reset = False for name, member in inspect.getmembers(environment, predicate=inspect.ismethod): if name == "reset": has_reset = True elif not name.startswith("_"): environment_methods[i].append(member) if not has_reset: raise ValueError( "Each environment instance returned by `environment_factory` must define a callable `reset` " ) else: self.environments = None tools = tools or [] self._sync_tool_dicts = [{} for _ in range(generation_batch_size)] self._async_tool_dicts = [{} for _ in range(generation_batch_size)] for i in range(generation_batch_size): for tool in tools + (environment_methods[i] if self.environments is not None else []): if inspect.iscoroutinefunction(tool): self._async_tool_dicts[i][tool.__name__] = tool else: self._sync_tool_dicts[i][tool.__name__] = tool self.tools = tools + (environment_methods[0] if self.environments is not None else []) # Check for async functions to start an event loop on a daemon thread self._has_async_funcs = any(inspect.iscoroutinefunction(func) for func in self.reward_funcs + self.tools) if self._has_async_funcs: self.async_loop_thread, self.async_loop, self.async_loop_ready_event = start_event_loop_in_daemon( name="GRPOTrainer-AsyncLoop" ) # wait until the event loop is running in the daemon thread self.async_loop_ready_event.wait() atexit.register(shutdown_event_loop_in_daemon, self.async_loop_thread, self.async_loop) # At the time of initial implementation, most tokenizers do not have built-in support for response schemas. # While waiting for broader adoption, we provide this utility function to manually set the response schema for # known chat templates. # We need `getattr`` until the base class sets a default None value for response_schema if self.tools and not getattr(processing_class, "response_schema", None): processing_class = add_response_schema(processing_class) # In multi-turn training, the chat template *must* be prefix-preserving. If the tokenizer's original template # isn't, we replace it at initialization with a training-safe, prefix-preserving template. if self.tools: self.chat_template = get_training_chat_template(processing_class) else: self.chat_template = None # Training arguments self.max_completion_length = args.max_completion_length # = |o_i| in the GRPO paper self.num_generations = args.num_generations # = G in the GRPO paper self.max_tool_calling_iterations = args.max_tool_calling_iterations or sys.maxsize self.num_generations_eval = args.num_generations_eval or self.num_generations self.chat_template_kwargs = args.chat_template_kwargs or {} self.temperature = args.temperature self.top_p = args.top_p self.top_k = args.top_k self.min_p = args.min_p self.repetition_penalty = args.repetition_penalty self.use_transformers_paged = args.use_transformers_paged self.pad_to_multiple_of = args.pad_to_multiple_of self.use_vllm = args.use_vllm self.vllm_mode = args.vllm_mode self.vllm_gpu_memory_utilization = args.vllm_gpu_memory_utilization # only applies to colocation mode self.vllm_tensor_parallel_size = args.vllm_tensor_parallel_size # only applies to colocation mode self.vllm_importance_sampling_correction = args.vllm_importance_sampling_correction self.vllm_importance_sampling_mode = args.vllm_importance_sampling_mode self.vllm_importance_sampling_cap = args.vllm_importance_sampling_cap self.use_liger_kernel = args.use_liger_kernel self.loss_type = args.loss_type self.multi_objective_aggregation = args.multi_objective_aggregation self.scale_rewards = args.scale_rewards self.importance_sampling_level = args.importance_sampling_level self.off_policy_mask_threshold = args.off_policy_mask_threshold if self.use_liger_kernel and self.off_policy_mask_threshold is not None: raise ValueError("Liger kernel does not support off-policy sequence masking yet.") self.mask_truncated_completions = args.mask_truncated_completions self.top_entropy_quantile = args.top_entropy_quantile if self.use_liger_kernel and self.top_entropy_quantile < 1.0: raise NotImplementedError( "Liger Kernels don't currently support masking token positions based on entropy." ) if self.use_liger_kernel and self.importance_sampling_level not in ("token", "sequence"): raise ValueError( f"Unknown importance sampling level: {self.importance_sampling_level}. " "Possible values are 'token' and 'sequence'." ) # Datasets self.shuffle_dataset = args.shuffle_dataset if train_dataset is None: raise ValueError("`train_dataset` is required") elif ( isinstance(train_dataset, IterableDataset) or isinstance(eval_dataset, IterableDataset) or ( isinstance(eval_dataset, dict) and any(isinstance(ds, IterableDataset) for ds in eval_dataset.values()) ) ): # See https://github.com/huggingface/trl/issues/3213 raise NotImplementedError( "Iterable datasets are not yet supported in GRPOTrainer. Please use a standard dataset instead." ) if args.loss_type == "luspo" and args.importance_sampling_level != "sequence": logger.warning( "When using `'luspo'` loss, `importance_sampling_level` should be set to `'sequence'` to mirror the " "paper's setup." ) if args.loss_type == "vespo" and args.importance_sampling_level != "token": logger.warning( "VESPO computes sequence-level importance weights internally. `importance_sampling_level` should be " "set to `'token'` (the default)." ) if self.loss_type == "vespo" and self.use_vllm and self.vllm_importance_sampling_correction: if self.vllm_importance_sampling_mode not in ["token_truncate", "token_mask"]: raise ValueError( f"VESPO loss requires `vllm_importance_sampling_mode` to be either 'token_truncate' or " f"'token_mask'. Got: {self.vllm_importance_sampling_mode}." ) # Multi-step self.num_iterations = args.num_iterations # = 𝜇 in the GRPO paper self.epsilon_low = args.epsilon self.epsilon_high = args.epsilon_high if args.epsilon_high is not None else args.epsilon # Tracks the number of iterations (forward + backward passes), including those within a grad accum cycle self._step = 0 # Buffer the batch to reuse generated outputs across multiple updates. For more details, see # `_get_train_sampler` and `_prepare_inputs`. self._buffered_inputs = None # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=identity, # No data collation is needed in GRPO train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, callbacks=callbacks, optimizers=optimizers, # In Trainer, `training_step` scales the loss by `gradient_accumulation_steps` only if `compute_loss_func` # is None. For DAPO, loss scaling instead depends on the total number of completions tokens across the # global accumulated batch. To control scaling ourselves, we must disable Trainer’s built-in scaling. The # simplest (though a bit hacky) way is to set `compute_loss_func` to any non-None value, which bypasses # that behavior without rewriting `training_step`. compute_loss_func="non-None value to disable scaling", ) # Reference model self.beta = args.beta if self.beta == 0.0: # If beta is 0.0, the reference model is not needed self.ref_model = None elif is_peft_model(model): # If PEFT is used, the reference model is not needed since the adapter can be disabled # to revert to the initial model. self.ref_model = None else: # For deepspeed, fsdp or non-distributed models, create a reference model from scratch model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if self.args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None self.ref_model = create_model_from_path(get_config_model_id(self.model.config), **model_init_kwargs) # Disable dropout in the models if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) # Cast LM Head To FP32 if args.cast_lm_head_to_fp32: def _cast_lm_head_to_fp32(target_model: PreTrainedModel): """Cast lm_head to fp32 while preserving embedding output dtype if tied.""" def cast_inputs_to_fp32(module, inputs): # Preserve other positional args and kwargs untouched if not inputs: return inputs return (inputs[0].to(torch.float32),) + inputs[1:] original_dtype_local = target_model.lm_head.weight.dtype target_model.lm_head = target_model.lm_head.float() target_model.lm_head.register_forward_pre_hook(cast_inputs_to_fp32) if target_model.config.tie_word_embeddings: def cast_outputs_to_original_dtype(module, args, output): return output.to(original_dtype_local) # Only cast activations; weights are now fp32 (intentional for numerical stability of logits) target_model.model.embed_tokens.register_forward_hook(cast_outputs_to_original_dtype) _cast_lm_head_to_fp32(model) if self.ref_model is not None: _cast_lm_head_to_fp32(self.ref_model) # Liger loss if self.use_liger_kernel: if not is_liger_kernel_available(): raise ImportError( "Liger is required to use `use_liger_kernel` as the GRPO loss. Run `pip install liger-kernel`." ) # redirect the model.module forward to the model forward to ensure pre-forward hooks are called self._forward_redirection = _ForwardRedirection() self.liger_grpo_loss = LigerFusedLinearGRPOLoss( beta=self.beta, epsilon_low=self.epsilon_low, epsilon_high=self.epsilon_high, temperature=self.temperature, use_ref_model=self.beta != 0.0, loss_type=self.loss_type, max_completion_length=self.max_completion_length, importance_sampling_level=self.importance_sampling_level, ) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 self._current_train_step_time = 0.0 self.log_completions = args.log_completions self.log_unique_prompts = args.log_unique_prompts self.num_completions_to_print = args.num_completions_to_print # Keep logs sized to the generation batch to record only outputs from the latest model update. self._logs = { "images": deque(maxlen=args.generation_batch_size), "prompt": deque(maxlen=args.generation_batch_size), "completion": deque(maxlen=args.generation_batch_size), "rewards": defaultdict(lambda: deque(maxlen=args.generation_batch_size)), "advantages": deque(maxlen=args.generation_batch_size), "extra": defaultdict(lambda: deque(maxlen=args.generation_batch_size)), } # Buffers for user-logged data from reward functions, flushed after gathering self._pending_extra_logs = defaultdict(list) self._pending_metrics = defaultdict(list) # Ensure each process receives a unique seed to prevent duplicate completions when generating with # transformers if num_generations exceeds per_device_train_batch_size. We could skip it if we use vLLM, but # it's safer to set it in all cases. set_seed(args.seed, device_specific=True) if self.use_vllm: # Initialize vLLM generation backend self.vllm_generation = VLLMGeneration( model=self.model, accelerator=self.accelerator, is_fsdp_enabled=self.is_fsdp_enabled, processing_class=self.processing_class, # vLLM configuration mode=args.vllm_mode, structured_outputs_regex=args.vllm_structured_outputs_regex, # Server mode configuration server_base_url=args.vllm_server_base_url, server_host=args.vllm_server_host, server_port=args.vllm_server_port, group_port=args.vllm_group_port, server_timeout=args.vllm_server_timeout, # Colocate mode configuration tensor_parallel_size=args.vllm_tensor_parallel_size, gpu_memory_utilization=args.vllm_gpu_memory_utilization, max_model_length=args.vllm_max_model_length, max_num_seqs=args.per_device_train_batch_size * args.vllm_tensor_parallel_size * args.steps_per_generation, enable_sleep_mode=args.vllm_enable_sleep_mode, model_impl=args.vllm_model_impl, # Generation configuration repetition_penalty=self.repetition_penalty, temperature=self.temperature, top_p=self.top_p, top_k=self.top_k, min_p=self.min_p, max_completion_length=self.max_completion_length, logprobs=0, # we only need the generated token logprobs for the importance sampling correction generation_kwargs=args.generation_kwargs, ) self._last_loaded_step = -1 # tag to avoid useless loading during grad accumulation else: generation_kwargs = { "max_new_tokens": self.max_completion_length, "do_sample": True, "pad_token_id": tokenizer.pad_token_id, "bos_token_id": tokenizer.bos_token_id, "eos_token_id": tokenizer.eos_token_id, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k, "min_p": self.min_p, "repetition_penalty": self.repetition_penalty, "cache_implementation": args.cache_implementation, } if args.generation_kwargs is not None: generation_kwargs.update(args.generation_kwargs) self.generation_config = GenerationConfig(**generation_kwargs) # Keep training-specific generation kwargs to overwrite model's original generation config self.generation_kwargs = generation_kwargs # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags to the model self.model.add_model_tags(self._tag_names) if self.ref_model is not None: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) elif self.is_fsdp_enabled: self.ref_model = prepare_fsdp(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) if args.sync_ref_model: if self.beta == 0.0: raise ValueError( "You passed `sync_ref_model=True` while `beta=0.0`, which means the reference model is not used " "during training. Consequently, GRPOTrainer does not create a `ref_model` instance, and there is " "nothing to synchronize. Please set `sync_ref_model=False`, or set `beta` to a non-zero value." ) if is_peft_model(model): raise NotImplementedError( "You passed `sync_ref_model=True` while using a PEFT model, which is currently not supported. " "With PEFT, GRPOTrainer does not keep a separate reference model in memory; instead, it recovers " "reference behavior by temporarily disabling the adapter. As a result, there is no standalone " "`ref_model` instance to synchronize. Use `sync_ref_model=False`, or opt for full fine-tuning if " "you need a synced reference model. If you need `sync_ref_model` to work with PEFT, please open a " "feature request at https://github.com/huggingface/trl/issues." ) self.add_callback(SyncRefModelCallback(ref_model=self.ref_model, accelerator=self.accelerator)) for i, reward_func in enumerate(self.reward_funcs): if isinstance(reward_func, PreTrainedModel): if self.is_deepspeed_enabled: self.reward_funcs[i] = prepare_deepspeed(reward_func, self.accelerator) else: # set device placement to True to make `prepare_model` move `reward_func` to device when using fsdp self.reward_funcs[i] = self.accelerator.prepare_model( reward_func, evaluation_mode=True, device_placement=True ) if self.accelerator.is_main_process and self.log_completions: os.makedirs(os.path.join(self.args.output_dir, "completions"), exist_ok=True) if self.args.log_completions_hub_repo is not None: repo_id = self.args.log_completions_hub_repo create_repo(repo_id, private=self.args.hub_private_repo, repo_type="dataset", exist_ok=True) template_path = pkg_resources.files("trl").joinpath("templates/completions_dataset_card.md") card_data = DatasetCardData( pretty_name="TRL Completion logs", tags=["trl", "trl-logs", "completions"], ) card = DatasetCard.from_template( card_data=card_data, template_path=str(template_path), repo_id=repo_id, hub_model_id=self.args.hub_model_id, ) card.push_to_hub(repo_id) self.commit_scheduler = CommitScheduler( repo_id=repo_id, repo_type="dataset", folder_path=f"{self.args.output_dir}/completions", every=2, # minutes allow_patterns=["*.parquet"], ) def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). In GRPOTrainer, we preprocess data, so using the model's signature columns doesn't # work. Instead, we set them to the columns expected by the `training_step` method, hence the override. if self._signature_columns is None: self._signature_columns = ["prompt", "image", "images"] # This method overrides `Trainer.get_train_dataloader` to support our custom batching strategy. # Instead of returning a standard per-step batch (i.e., `per_device_batch_size), our dataloader loads an # *generation* batch (i.e., `per_device_batch_size × steps_per_generation`). This allows us to generate completions # once every steps_per_generation step—rather than once per accumulation step—which is significantly more # efficient. The only change from the original implementation is multiplying the batch size by # `steps_per_generation`. Thus, `_prepare_inputs` is called with this *generation* batch, and it handles the # splitting internally. # Maintenance note: This method is a copy-paste of the original `Trainer.get_train_dataloader` with only one line # modification. def get_train_dataloader(self): return self._get_dataloader( dataset=self.train_dataset, description="Training", batch_size=self._train_batch_size * self.args.steps_per_generation, # < this is the change sampler_fn=self._get_train_sampler, is_training=True, ) def _get_train_sampler(self, dataset: Dataset | None = None) -> Sampler: # Returns a sampler that # 1. ensures each prompt is repeated across multiple processes. This guarantees that identical prompts are # distributed to different GPUs, allowing rewards to be computed and normalized correctly within each prompt # group. Using the same seed across processes ensures consistent prompt assignment, preventing discrepancies # in group formation. # 2. repeats the batch multiple times to allow reusing generations across multiple updates. Refer to # _prepare_inputs to see how the generations are stored and reused. # In the following figure, the values are the prompt indices. The first row shows the first sampled batch, the # second row shows the second sampled batch, and so on. # # | GPU 0 | GPU 1 | # # global_step step <-───> num_generations=2 # <-───────> per_device_train_batch_size=3 # grad_accum ▲ ▲ 0 0 0 0 1 1 2 2 <- Generate for the first `steps_per_generation` (prompts 0 to 11); store the completions; use the first slice to compute the loss # =2 ▼ | 0 1 3 3 4 4 5 5 <- Take the stored generations and use the second slice to compute the loss # | # | 1 2 6 6 7 7 8 8 <- Take the stored generations and use the third slice to compute the loss # steps_per_gen=4 ▼ 1 3 9 9 10 10 11 11 <- Take the stored generations and use the fourth slice to compute the loss # # 2 4 12 12 13 13 14 14 <- Generate for the second `steps_per_generation` (prompts 12 to 23); store the completions; use the first slice to compute the loss # 2 5 15 15 16 16 17 17 <- Take the stored generations and use the second slice to compute the loss # ... if dataset is None: dataset = self.train_dataset return RepeatSampler( data_source=dataset, mini_repeat_count=self.num_generations, batch_size=self.args.generation_batch_size // self.num_generations, repeat_count=self.num_iterations * self.args.steps_per_generation, shuffle=self.shuffle_dataset, seed=self.args.seed, ) def _get_eval_sampler(self, eval_dataset) -> Sampler: # See _get_train_sampler for an explanation of the sampler. return RepeatSampler( data_source=eval_dataset, mini_repeat_count=self.num_generations_eval, seed=self.args.seed, ) @profiling_decorator def _get_last_hidden_state( self, unwrapped_model, input_ids, attention_mask, logits_to_keep, pixel_values=None, image_grid_thw=None, pixel_attention_mask=None, image_sizes=None, ): if is_peft_model(unwrapped_model): unwrapped_model = unwrapped_model.base_model.model # Build model inputs - check if the model supports logits_to_keep (some models and VLMs don't) model_inputs = {"input_ids": input_ids, "attention_mask": attention_mask} # For Qwen models: if image_grid_thw is not None and pixel_values is not None: model_inputs["image_grid_thw"] = image_grid_thw # For Gemma, SmolVLM2, LLaVa-Next etc.: if pixel_values is not None: model_inputs["pixel_values"] = pixel_values # For SmolVLM2 if pixel_attention_mask is not None: model_inputs["pixel_attention_mask"] = pixel_attention_mask # For LLaVa-Next if image_sizes is not None: model_inputs["image_sizes"] = image_sizes # Only add logits_to_keep if the model supports it if "logits_to_keep" in self.model_kwarg_keys: # We add 1 to `logits_to_keep` because the last logits of the sequence is later excluded model_inputs["logits_to_keep"] = logits_to_keep + 1 model_inputs["use_cache"] = False # only used in generation; set False to suppress warnings last_hidden_state = unwrapped_model.model(**model_inputs).last_hidden_state # Exclude the last value: it corresponds to the next token pred last_hidden_state = last_hidden_state[:, :-1, :] # (B, L-1, H) # Only keep the last logits_to_keep. For model that support logits_to_keep, this is a no-op. last_hidden_state = last_hidden_state[:, -logits_to_keep:, :] # (B, logits_to_keep, H) return last_hidden_state def get_high_entropy_mask(self, entropies: torch.Tensor, mask: torch.Tensor, threshold: float) -> torch.Tensor: """ Returns a binary mask identifying tokens whose entropy exceeds a given quantile threshold. Args: entropies (`torch.Tensor`): Tensor of shape (batch_size, seq_len) with per-token entropy values. mask (`torch.Tensor`): Binary mask of the same shape as `entropies`, where `1` indicates valid tokens and `0` padding. threshold (`float`): Quantile threshold between `0.0` and `1.0` to select high-entropy tokens. Returns: `torch.Tensor`: Boolean mask of shape (batch_size, seq_len), where `True` indicates tokens with entropy >= threshold and `False` otherwise. """ local = entropies[mask.bool()].float() # Use a negative pad_value as a sentinel because entropy values are always >= 0. # This guarantees that the sentinel cannot collide with any real entropy value. pad_value = -1e9 # Pad across processes so that every rank has the same tensor length padded = self.accelerator.pad_across_processes(local, dim=0, pad_index=pad_value) gathered = self.accelerator.gather(padded) # Drop sentinel values (safe because no entropy can be negative) gathered = gathered[gathered != pad_value] if gathered.numel() == 0: return torch.zeros_like(entropies, dtype=torch.bool) entropy_threshold = torch.quantile(gathered, threshold) masked_entropies = entropies * mask.float() entropy_mask = masked_entropies >= entropy_threshold return entropy_mask & mask.bool() # ensure padding tokens are always masked out @profiling_decorator def _get_per_token_logps_and_entropies( self, model, input_ids, attention_mask, logits_to_keep, batch_size=None, compute_entropy=False, pixel_values=None, image_grid_thw=None, num_images=None, pixel_attention_mask=None, image_sizes=None, token_type_ids=None, mm_token_type_ids=None, ) -> dict[str, torch.Tensor | None]: """Compute log-probs and (optionally) entropies for each token.""" batch_size = batch_size or input_ids.size(0) # Chunk inputs into smaller batches to reduce memory peak all_logps = [] all_entropies = [] for start in range(0, input_ids.size(0), batch_size): input_ids_batch = input_ids[start : start + batch_size] attention_mask_batch = attention_mask[start : start + batch_size] # Build model inputs - check if the model supports logits_to_keep (some models and VLMs don't) model_inputs = {"input_ids": input_ids_batch, "attention_mask": attention_mask_batch} if image_grid_thw is not None and pixel_values is not None: rows_per_image = image_grid_thw.prod(dim=-1) rows_per_sample = torch.split(rows_per_image, num_images) rows_per_sample = torch.stack([s.sum() for s in rows_per_sample]) cum_rows = torch.cat([torch.tensor([0], device=rows_per_sample.device), rows_per_sample.cumsum(0)]) row_start, row_end = cum_rows[start].item(), cum_rows[start + batch_size].item() model_inputs["pixel_values"] = pixel_values[row_start:row_end] cum_imgs = torch.tensor([0] + num_images).cumsum(0) img_start, img_end = cum_imgs[start], cum_imgs[start + batch_size] model_inputs["image_grid_thw"] = image_grid_thw[img_start:img_end] elif pixel_values is not None: model_inputs["pixel_values"] = pixel_values[start : start + batch_size] if pixel_attention_mask is not None: model_inputs["pixel_attention_mask"] = pixel_attention_mask[start : start + batch_size] if image_sizes is not None: model_inputs["image_sizes"] = image_sizes[start : start + batch_size] if token_type_ids is not None: model_inputs["token_type_ids"] = token_type_ids[start : start + batch_size] if mm_token_type_ids is not None: model_inputs["mm_token_type_ids"] = mm_token_type_ids[start : start + batch_size] # Only add logits_to_keep if the model supports it if "logits_to_keep" in self.model_kwarg_keys: # We add 1 to `logits_to_keep` because the last logits of the sequence is later excluded model_inputs["logits_to_keep"] = logits_to_keep + 1 model_inputs["use_cache"] = False # only used in generation; set False to suppress warnings logits = model(**model_inputs).logits # Exclude the last value: it corresponds to the next token pred logits = logits[:, :-1, :] # (B, L-1, H) # Only keep the last logits_to_keep. For model that support logits_to_keep, this is a no-op. logits = logits[:, -logits_to_keep:, :] # (B, logits_to_keep, H) # Divide logits by sampling temperature. # See https://huggingface.co/blog/the_n_implementation_details_of_rlhf_with_ppo#policy-training-implementation-details logits.div_(self.temperature) completion_ids = input_ids_batch[:, -logits_to_keep:] logps = selective_log_softmax(logits, completion_ids) # compute logprobs all_logps.append(logps) if compute_entropy: with torch.no_grad(): entropies = entropy_from_logits(logits) all_entropies.append(entropies) logps = torch.cat(all_logps, dim=0) entropies = torch.cat(all_entropies, dim=0) if compute_entropy else None return logps, entropies def training_step(self, model, inputs, num_items_in_batch): time_before = time.perf_counter() output = super().training_step(model, inputs, num_items_in_batch) self._step += 1 time_after = time.perf_counter() self._current_train_step_time += time_after - time_before if self._step % self.current_gradient_accumulation_steps == 0: self._metrics["train"]["step_time"].append(self._current_train_step_time) self._current_train_step_time = 0.0 return output @profiling_decorator def _prepare_inputs(self, generation_batch: dict[str, torch.Tensor | Any]) -> dict[str, torch.Tensor | Any]: # Prepares inputs for model training/evaluation by managing completion generation and batch handling. # During training: # - Receives the local generation batch (Per-GPU batch size × steps per generation) # from the modified training dataloader instead of the standard local batch # - Generates completions once for the entire generation batch and splits it into batches of size # `per_device_train_batch_size` # - Buffers these completions and returns the appropriate slice for the current accumulation step # - Optimizes by regenerating completions only periodically (every steps_per_generation * num_iterations) # During evaluation: # - The input is treated as a standard local batch (no accumulation, no multiple iterations) # - Completions are generated for each batch without buffering or reuse # Returns a single local batch in both cases. mode = "train" if self.model.training else "eval" if mode == "train": generate_every = self.args.steps_per_generation * self.num_iterations if self._step % generate_every == 0 or self._buffered_inputs is None: # self._buffered_inputs=None can occur when resuming from a checkpoint generation_batch = self._generate_and_score_completions(generation_batch) generation_batch = split_pixel_values_by_grid(generation_batch) generation_batch = shuffle_sequence_dict(generation_batch) generation_batches = split_tensor_dict(generation_batch, self.args.steps_per_generation) self._buffered_inputs = [unsplit_pixel_values_by_grid(batch) for batch in generation_batches] inputs = self._buffered_inputs[self._step % self.args.steps_per_generation] else: # In evaluation, there is neither batch grouping for generation, nor multiple iterations, hence # local generation batch == local eval batch inputs = self._generate_and_score_completions(generation_batch) return inputs def _log_completion_extra(self, column: str, values: list): """ Log extra columns to the completions table. Called from reward functions via the `log_extra` kwarg. Args: column (`str`): Name of the column to add. values (`list`): Values for the column, one per sample in the batch. """ self._pending_extra_logs[column].extend(values) def _log_metric(self, name: str, value: float): """ Log a scalar metric from a reward function. Called via the `log_metric` kwarg. Values are averaged over each logging step and reported alongside built-in metrics like `kl` and `entropy`. Args: name (`str`): Name of the metric. value (`float`): Scalar value for this batch. """ self._pending_metrics[name].append(value) @profiling_decorator def _calculate_rewards(self, inputs, prompts, completions, completion_ids_list): device = self.accelerator.device rewards_per_func = torch.zeros(len(prompts), len(self.reward_funcs), device=device) # Repeat all input columns (but "prompt", "completion", and "completion_ids") to match the num of generations keys = [key for key in inputs[0] if key not in ["prompt", "completion", "completion_ids"]] reward_kwargs = {key: [example[key] for example in inputs] for key in keys} # This allows for dynamic reward shaping based on training progress. reward_kwargs["trainer_state"] = self.state # Allow reward functions to log extra columns to the completions table. reward_kwargs["log_extra"] = self._log_completion_extra # Allow reward functions to log additional scalar metrics. reward_kwargs["log_metric"] = self._log_metric async_funcs_info = [] # async custom functions for asyncio.gather for i, (reward_func, reward_processing_class, reward_func_name) in enumerate( zip(self.reward_funcs, self.reward_processing_classes, self.reward_func_names, strict=True) ): if isinstance(reward_func, nn.Module): # Module (no PretrainedModel) for compat with compiled models with profiling_context(self, reward_func_name): if is_conversational(inputs[0]): messages = [{"messages": p + c} for p, c in zip(prompts, completions, strict=True)] texts = [ apply_chat_template(x, reward_processing_class, **self.chat_template_kwargs)["text"] for x in messages ] else: texts = [p + c for p, c in zip(prompts, completions, strict=True)] reward_inputs = reward_processing_class( text=texts, return_tensors="pt", padding=True, padding_side="right", add_special_tokens=False ) reward_inputs = super()._prepare_inputs(reward_inputs) with torch.inference_mode(): rewards_per_func[:, i] = reward_func(**reward_inputs).logits[:, 0] # Shape (B*G,) elif inspect.iscoroutinefunction(reward_func): # Separate async reward funcs to run them in parallel later async_funcs_info.append((i, reward_func, reward_func_name)) else: # Run synchronous reward function with profiling_context(self, reward_func_name): if self.environments is not None: reward_kwargs["environments"] = self.environments output_reward_func = reward_func( prompts=prompts, completions=completions, completion_ids=completion_ids_list, **reward_kwargs ) # Convert None values to NaN output_reward_func = [reward if reward is not None else torch.nan for reward in output_reward_func] rewards_per_func[:, i] = torch.tensor(output_reward_func, dtype=torch.float32, device=device) # Execute async custom functions in parallel using asyncio.gather if async_funcs_info: async def _invoke_async(index, func, func_name): with profiling_context(self, func_name): output = await func( prompts=prompts, completions=completions, completion_ids=completion_ids_list, **reward_kwargs ) output = [r if r is not None else torch.nan for r in output] return index, output async def _run_async_funcs(): coros = [_invoke_async(i, func, func_name) for (i, func, func_name) in async_funcs_info] return await asyncio.gather(*coros) async_results = asyncio.run_coroutine_threadsafe(_run_async_funcs(), self.async_loop).result() for idx, output_reward_func in async_results: rewards_per_func[:, idx] = torch.tensor(output_reward_func, dtype=torch.float32, device=device) # If all reward functions return None for a given row, issue a detailed warning if torch.isnan(rewards_per_func).all(dim=1).any(): nan_row_idx = torch.isnan(rewards_per_func).all(dim=1).nonzero(as_tuple=True)[0][0] row_reward_kwargs = { key: value[nan_row_idx] for key, value in reward_kwargs.items() if key not in ("trainer_state", "log_extra", "log_metric") } row_reward_kwargs["prompt"] = prompts[nan_row_idx] row_reward_kwargs["completion"] = completions[nan_row_idx] logger.warning( f"All reward functions returned None for the following kwargs:\n{row_reward_kwargs}\n" "Please ensure that at least one reward function returns a valid reward." ) # Gather the reward per function: this part is crucial, because the rewards are normalized per group and the # completions may be distributed across processes rewards_per_func = gather(rewards_per_func) return rewards_per_func def _tokenize_prompts(self, prompts: list): """Tokenize prompts and extract images/multimodal fields for generation.""" if is_conversational({"prompt": prompts[0]}): # Extract images from messages for VLM support images = [] has_images = False for prompt in prompts: prompt_images = [] for message in prompt: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image": prompt_images.append(part["image"]) has_images = True images.append(prompt_images if prompt_images else None) images = images if has_images else None # We pass padding=True to work around a bug introduced in transformers 5.2.0 in some processors # (e.g. Qwen2.5-VL) that crash on batched unpadded input. We then unpad input_ids using attention_mask. # See: https://github.com/huggingface/transformers/issues/44514 tokenized = self.processing_class.apply_chat_template( conversation=prompts, tools=self.tools, chat_template=self.chat_template, add_generation_prompt=True, tokenize=True, return_dict=True, padding=True, **self.chat_template_kwargs, ) # Unpad input_ids: remove padding tokens using attention_mask to get per-sequence lists prompt_ids = [ [tok for tok, m in zip(ids, mask, strict=True) if m] for ids, mask in zip(tokenized["input_ids"], tokenized["attention_mask"], strict=True) ] # For VLMs, the processor returns extra multimodal fields (pixel_values, image_grid_thw, etc.) multimodal_fields = {k: v for k, v in tokenized.items() if k not in ("input_ids", "attention_mask")} else: prompt_ids = self.processing_class(text=prompts)["input_ids"] images = None multimodal_fields = {} return prompt_ids, images, multimodal_fields def _generate_single_turn(self, prompt_ids, images, multimodal_fields): device = self.accelerator.device mode = "train" if self.model.training else "eval" # Generate completions using either vLLM or regular generation if self.use_vllm: # Sync weights if training step changed if self.state.global_step != self._last_loaded_step: with profiling_context(self, "sync_weights"): self.vllm_generation.sync_weights() self._last_loaded_step = self.state.global_step # Generate using vLLM with raw token IDs num_generations = self.num_generations if mode == "train" else self.num_generations_eval _, completion_ids, logprobs, _ = self.vllm_generation.generate( prompts=prompt_ids, images=images, num_generations=num_generations, profiler=profiling_context(self, "vLLM.generate"), ) # vLLM returns per-token top-k logprobs; keep only the top-1 (sampled token) logprob logprobs = [[lp[0] for lp in seq] for seq in logprobs] elif self.use_transformers_paged: with ( profiling_context(self, "transformers.generate_batch"), unwrap_model_for_generation( self.model_wrapped, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): # Cast to the appropriate dtype based on training configuration if self.args.bf16: unwrapped_model.to(torch.bfloat16) elif self.args.fp16: unwrapped_model.to(torch.float16) if self.args.cast_lm_head_to_fp32: unwrapped_model.lm_head.to(torch.float32) with torch.inference_mode(): # Continuous batching API expects 'inputs' arg only all_outputs = unwrapped_model.generate_batch( prompt_ids, generation_config=self.generation_config, progress_bar=False ) unwrapped_model.train() # restore training mode, as generate_batch forces eval mode completion_ids = [output.generated_tokens for output in all_outputs.values()] logprobs = None # not used in this case else: # Regular generation path: left-pad token IDs into tensors prompt_tensors = [torch.tensor(ids) for ids in prompt_ids] padded_ids = pad(prompt_tensors, padding_value=self.pad_token_id, padding_side="left") attention_mask = pad([torch.ones_like(t) for t in prompt_tensors], padding_value=0, padding_side="left") generate_inputs = {"input_ids": padded_ids, "attention_mask": attention_mask} # For VLMs, include multimodal fields as tensors (pixel_values, image_grid_thw, etc.) for k, v in multimodal_fields.items(): if isinstance(v, torch.Tensor): generate_inputs[k] = v elif isinstance(v, list) and v and isinstance(v[0], list): # Per-token field (e.g., token_type_ids): left-pad like input_ids generate_inputs[k] = pad([torch.tensor(x) for x in v], padding_value=0, padding_side="left") else: generate_inputs[k] = torch.tensor(np.array(v)) generate_inputs = super()._prepare_inputs(generate_inputs) with ( profiling_context(self, "transformers.generate"), unwrap_model_for_generation( self.model_wrapped, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): prompt_completion_ids = unwrapped_model.generate( **generate_inputs, generation_config=self.generation_config, disable_compile=True ) # Compute prompt length and extract completion ids prompt_length = generate_inputs["input_ids"].size(1) completion_ids = prompt_completion_ids[:, prompt_length:] # Mask everything after the first EOS token is_eos = completion_ids == self.eos_token_id eos_idx = torch.full((is_eos.size(0),), is_eos.size(1), dtype=torch.long, device=device) eos_idx[is_eos.any(dim=1)] = is_eos.int().argmax(dim=1)[is_eos.any(dim=1)] sequence_indices = torch.arange(is_eos.size(1), device=device).expand(is_eos.size(0), -1) completion_mask = (sequence_indices <= eos_idx.unsqueeze(1)).int() completion_ids = [ c[m].tolist() for c, m in zip(completion_ids.cpu(), completion_mask.bool().cpu(), strict=True) ] logprobs = None # not used in this case return completion_ids, logprobs def _get_tool_suffix_ids(self, tool_messages): """Get token IDs for tool result formatting by using a minimal dummy conversation.""" dummy_messages = [{"role": "user", "content": "dummy"}, {"role": "assistant", "content": "dummy"}] prefix_ids = self.processing_class.apply_chat_template( dummy_messages, add_generation_prompt=False, chat_template=self.chat_template, return_dict=False, **self.chat_template_kwargs, ) full_ids = self.processing_class.apply_chat_template( dummy_messages + tool_messages, add_generation_prompt=True, chat_template=self.chat_template, return_dict=False, **self.chat_template_kwargs, ) if not full_ids[: len(prefix_ids)] == prefix_ids: raise ValueError("Unexpected tokenization: the prefix IDs are not a prefix of the full IDs.") return full_ids[len(prefix_ids) :] def _tool_call_loop(self, prompts, prompt_ids, completion_ids, completions, logprobs, images, multimodal_fields): # Tool execution loop: execute tools, then regenerate completions with tool results appended to the prompt tool_calls = [completion[0].get("tool_calls") for completion in completions] idxs_with_tool = [idx for idx, tool_call in enumerate(tool_calls) if tool_call] tool_calls = [tool_calls[idx] for idx in idxs_with_tool] tool_mask = [[1] * len(ids) for ids in completion_ids] # 0 for tool result tokens, 1 elsewhere tool_call_count = 0 tool_failure_count = 0 iteration_num = 0 while idxs_with_tool and iteration_num < self.max_tool_calling_iterations: prompt_completion_tools = [prompts[i] for i in idxs_with_tool] # select only prompts that need tool calls # Call the tools, and build the new prompt for generation for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] tool_call_list = tool_calls[idx] prompt_completion_tool = prompt_completion_tools[idx] sync_tool_dict = self._sync_tool_dicts[idx_with_tool] async_tool_dict = self._async_tool_dicts[idx_with_tool] # Append the last assistant message (which triggered tool_calls) to the prompt prompt_completion_tool.append(completions[idx_with_tool][-1]) async_coros = [] tool_call_results = [] for tool_call in tool_call_list: tool_call_count += 1 if tool_call["type"] == "function": function = tool_call["function"] name = function["name"] try: if name in sync_tool_dict: tool_call_results.append((name, sync_tool_dict[name](**function["arguments"]))) elif name in async_tool_dict: async_coros.append((name, async_tool_dict[name](**function["arguments"]))) else: raise ValueError(f"Tool {name} not found.") except Exception as e: tool_failure_count += 1 result = {"error": str(e)} tool_call_results.append((name, result)) else: tool_failure_count += 1 name = tool_call.get("name", "unknown") tool_call_results.append((name, {"error": f"Unsupported tool call type: {tool_call['type']}"})) if async_coros: async def _run_async_tools(async_coros): coros = [coro for _, coro in async_coros] results = await asyncio.gather(*coros, return_exceptions=True) return [(name, result) for (name, _), result in zip(async_coros, results, strict=False)] async_results = asyncio.run_coroutine_threadsafe( _run_async_tools(async_coros), self.async_loop ).result() for name, result in async_results: if isinstance(result, Exception): tool_failure_count += 1 tool_call_results.append((name, {"error": str(result)})) else: tool_call_results.append((name, result)) for name, result in tool_call_results: tool_message = {"role": "tool", "name": name, "content": str(result)} prompt_completion_tool.append(tool_message) completions[idx_with_tool].append(tool_message) # Build token IDs by concatenation: prompt + completion + tool_suffix. prompt_completion_tool_ids = [] for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] # Extract trailing tool messages from completions tool_messages = [] for message in reversed(completions[idx_with_tool]): if message["role"] == "tool": tool_messages.insert(0, message) else: break suffix_ids = self._get_tool_suffix_ids(tool_messages) prompt_completion_tool_ids.append( prompt_ids[idx_with_tool] + completion_ids[idx_with_tool] + suffix_ids ) # Filter samples whose length exceeds max allowed length. This is important, because both # vLLM and transformers will error out if the input is longer than the model's max length. if self.use_vllm and self.vllm_mode == "colocate": max_model_len = self.vllm_generation.llm.llm_engine.model_config.max_model_len elif not self.use_vllm: max_model_len = self.model.config.max_position_embeddings else: raise NotImplementedError( f"Unsupported mode detected: use_vllm={self.use_vllm}, vllm_mode={self.vllm_mode}" ) overlong = [len(pct) >= max_model_len for pct in prompt_completion_tool_ids] for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] if overlong[idx]: prompt_length = len(prompt_ids[idx_with_tool]) ct = prompt_completion_tool_ids[idx][prompt_length : prompt_length + self.max_completion_length] completion_ids[idx_with_tool] = ct tool_mask[idx_with_tool] += [1] * (len(ct) - len(tool_mask[idx_with_tool])) if logprobs is not None: logprobs[idx_with_tool] += [0.0] * (len(ct) - len(logprobs[idx_with_tool])) # Keep only non-overlong items for further processing idxs_with_tool = [idx for idx, o in zip(idxs_with_tool, overlong, strict=True) if not o] prompt_completion_tools = [pct for pct, o in zip(prompt_completion_tools, overlong, strict=True) if not o] prompt_completion_tool_ids = [ pct for pct, o in zip(prompt_completion_tool_ids, overlong, strict=True) if not o ] if not idxs_with_tool: break # all overlong, exit tool loop # Filter images and multimodal fields to match the current subset (index into full batch) loop_images = [images[i] for i in idxs_with_tool] if images else None loop_multimodal_fields = ( {k: [v[i] for i in idxs_with_tool] for k, v in multimodal_fields.items()} if multimodal_fields else {} ) # Generate new completions after tool execution (using concatenated IDs, no re-tokenization) post_tool_ids, post_tool_logprobs = self._generate_single_turn( prompt_completion_tool_ids, loop_images, loop_multimodal_fields ) # Truncate so that pct[len(prompt_ids[idx]) :] + post_tool does not exceed max_completion_length for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_len = len(prompt_ids[idx_with_tool]) completion_tool_ids = prompt_completion_tool_ids[idx][prompt_len:] excess_length = len(completion_tool_ids) + len(post_tool_ids[idx]) - self.max_completion_length if excess_length > 0: # If exceeding max length, truncate post_tool_ids post_tool_ids[idx] = post_tool_ids[idx][:-excess_length] if logprobs is not None: post_tool_logprobs[idx] = post_tool_logprobs[idx][:-excess_length] excess_length = len(completion_tool_ids) + len(post_tool_ids[idx]) - self.max_completion_length if excess_length > 0: # If still exceeding max length, truncate completion_tool_ids as well prompt_completion_tool_ids[idx] = prompt_completion_tool_ids[idx][:-excess_length] # Update tool_mask: the tool result should be 0 and the post-tool 1 for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_completion_tool_length = len(prompt_completion_tool_ids[idx]) prompt_length = len(prompt_ids[idx_with_tool]) completion_length = len(completion_ids[idx_with_tool]) post_tool_length = len(post_tool_ids[idx]) tool_length = prompt_completion_tool_length - prompt_length - completion_length tool_mask[idx_with_tool] += [0] * tool_length + [1] * post_tool_length if logprobs is not None: logprobs[idx_with_tool] += [0.0] * tool_length + post_tool_logprobs[idx] # Update completion_ids with the new completions (after tool execution) for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] prompt_length = len(prompt_ids[idx_with_tool]) pct = prompt_completion_tool_ids[idx] # = prompt-completion-tool completion_ids[idx_with_tool] = pct[prompt_length:] + post_tool_ids[idx] # Decode post-tool completions post_tool_completions = [ parse_response(self.processing_class, ids) if ids else {} for ids in post_tool_ids ] # Add post-tool completions to the existing completions for idx in range(len(idxs_with_tool)): idx_with_tool = idxs_with_tool[idx] if post_tool_completions[idx]: # {} if post-tool completions completely truncated completions[idx_with_tool].append(post_tool_completions[idx]) # Check for further tool calls tool_calls = [completion.get("tool_calls") for completion in post_tool_completions] idxs_with_tool = [idx for idx, tool_call in zip(idxs_with_tool, tool_calls, strict=True) if tool_call] tool_calls = [tool_call for tool_call in tool_calls if tool_call] iteration_num += 1 return tool_mask, completions, completion_ids, logprobs, tool_call_count, tool_failure_count def _generate(self, prompts: list): device = self.accelerator.device mode = "train" if self.model.training else "eval" # Copy the prompts to avoid modifying the original list prompts = copy.deepcopy(prompts) if self.rollout_func is not None: # Keep vLLM weights in sync for custom rollouts that rely on vLLM utilities. if self.use_vllm and self.state.global_step != self._last_loaded_step: with profiling_context(self, "sync_weights"): self.vllm_generation.sync_weights() self._last_loaded_step = self.state.global_step # Pass prompts to rollout_func preserving structured messages. # Chat templating must happen inside rollout_func, at the backend boundary, so that # multimodal content (images, typed content blocks) is not lost before rollout logic runs. output = self.rollout_func(prompts, self) required_keys = {"prompt_ids", "completion_ids", "logprobs"} missing_keys = required_keys - output.keys() if missing_keys: missing_keys_list = sorted(missing_keys) raise ValueError(f"rollout_func must return keys {missing_keys_list} in its output dict.") extra_fields = {k: v for k, v in output.items() if k not in required_keys} prompt_ids, completion_ids, logprobs = output["prompt_ids"], output["completion_ids"], output["logprobs"] else: prompt_ids, images, multimodal_fields = self._tokenize_prompts(prompts) completion_ids, logprobs = self._generate_single_turn(prompt_ids, images, multimodal_fields) extra_fields = {} # Decode completions. It's important to use `parse_response` when possible, because it handles tool calls. if is_conversational({"prompt": prompts[0]}): if ( Version(transformers.__version__) >= Version("5.0.0") # parse_response added in v5 and isinstance(self.processing_class, PreTrainedTokenizerBase) # doesn't work with processors and hasattr(self.processing_class, "response_schema") # attribute not set by default for now and self.processing_class.response_schema is not None # only works if the tokenizer has a schema ): completions = [[parse_response(self.processing_class, ids)] for ids in completion_ids] else: contents = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) completions = [[{"role": "assistant", "content": content}] for content in contents] else: completions = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Extract tool calls from the completions and (possibly) execute them if self.tools: ( tool_mask, completions, completion_ids, logprobs, tool_call_count, tool_failure_count, ) = self._tool_call_loop( prompts, prompt_ids, completion_ids, completions, logprobs, images, multimodal_fields ) else: # Support custom env_mask from rollout_func (e.g., for environment feedback masking) # Internally treated as tool_mask - marks model tokens (1) vs external tokens (0) tool_mask = extra_fields.pop("env_mask", None) # Get completion length per sequence, used for logging prompt_lengths = torch.tensor([len(ids) for ids in prompt_ids], device=device) if tool_mask is not None: # count only model-generated tokens (tool_mask=1) completion_lengths = torch.tensor([sum(mask) for mask in tool_mask], device=device) else: completion_lengths = torch.tensor([len(ids) for ids in completion_ids], device=device) agg_prompt_lengths = self.accelerator.gather(prompt_lengths) agg_completion_lengths = self.accelerator.gather(completion_lengths) total_prompt_tokens = agg_prompt_lengths.sum() total_completion_tokens = agg_completion_lengths.sum() # = num_items_in_batch, required for the DAPO loss # Log the metrics if mode == "train": self.state.num_input_tokens_seen += (total_prompt_tokens + total_completion_tokens).item() self._metrics[mode]["num_tokens"] = [self.state.num_input_tokens_seen] # Log completion lengths, mean, min, max self._metrics[mode]["completions/mean_length"].append(agg_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_length"].append(agg_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_length"].append(agg_completion_lengths.float().max().item()) # Identify sequences that terminated with EOS and log their lengths eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids], device=device) agg_is_truncated = self.accelerator.gather(is_truncated) self._metrics[mode]["completions/clipped_ratio"].append(agg_is_truncated.float().mean().item()) term_completion_lengths = agg_completion_lengths[~agg_is_truncated] if len(term_completion_lengths) == 0: # edge case where no terminated sequences are found term_completion_lengths = torch.zeros(1, device=device) self._metrics[mode]["completions/mean_terminated_length"].append(term_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_terminated_length"].append(term_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_terminated_length"].append(term_completion_lengths.float().max().item()) if self.tools: agg_tool_call_count = self.accelerator.gather(torch.tensor(tool_call_count, device=device)).sum() tool_call_frequency = (agg_tool_call_count / len(agg_prompt_lengths)).item() self._metrics[mode]["tools/call_frequency"].append(tool_call_frequency) agg_tool_failure_count = self.accelerator.gather(torch.tensor(tool_failure_count, device=device)).sum() failure_frequency = ( (agg_tool_failure_count / agg_tool_call_count).item() if agg_tool_call_count > 0 else 0.0 ) self._metrics[mode]["tools/failure_frequency"].append(failure_frequency) return ( prompt_ids, completion_ids, tool_mask, completions, total_completion_tokens, logprobs, extra_fields, ) def _generate_and_score_completions( self, inputs: list[dict[str, torch.Tensor | Any]] ) -> dict[str, torch.Tensor | Any]: device = self.accelerator.device mode = "train" if self.model.training else "eval" prompts = [x["prompt"] for x in inputs] if self.environments: for prompt, environment, reset_kwargs in zip(prompts, self.environments, inputs, strict=True): observation = environment.reset(**reset_kwargs) if observation is None: continue prompt[-1]["content"] += observation if "images" in inputs[0]: images = [example.get("images") for example in inputs] elif "image" in inputs[0]: images = [[example.get("image")] if example.get("image") is not None else None for example in inputs] else: images = None # Transformers requires at least one image in the batch, otherwise it throws an error if images is not None and all(img_list == [] for img_list in images): images = None # If the prompts are conversational and the inputs contain images, we need to convert the prompts from # [{"role": "user", "content": "What color is the sky?"}] to # [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What color is the sky?"}]}] if images is not None: if not is_conversational(inputs[0]): raise ValueError( "Multimodal training requires conversational prompts. It looks like the dataset contains " "non-conversational inputs, likely because a chat template was applied before passing the dataset " "to the trainer. Please provide the raw conversational prompts and let the trainer apply the chat " "template internally." ) prompts = [ prepare_multimodal_messages(prompt, image_list) for prompt, image_list in zip(prompts, images, strict=True) ] ( prompt_ids_list, completion_ids_list, tool_mask_list, completions, num_items_in_batch, sampling_per_token_logps_list, extra_fields, ) = self._generate(prompts) # Convert lists of token IDs to padded tensors prompt_ids = [torch.tensor(ids) for ids in prompt_ids_list] prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] prompt_ids = pad( prompt_ids, padding_value=self.pad_token_id, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) prompt_mask = pad( prompt_mask, padding_value=0, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) completion_ids = [torch.tensor(ids) for ids in completion_ids_list] completion_mask = [torch.ones_like(ids, dtype=torch.long) for ids in completion_ids] completion_ids = pad( completion_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) if sampling_per_token_logps_list is not None: sampling_per_token_logps = [torch.tensor(logps) for logps in sampling_per_token_logps_list] sampling_per_token_logps = pad( sampling_per_token_logps, padding_value=0.0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) else: sampling_per_token_logps = None if tool_mask_list is not None: tool_mask = [torch.tensor(mask) for mask in tool_mask_list] tool_mask = pad( tool_mask, padding_value=1, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ).to(device=device) else: tool_mask = None # If mask_truncated_completions is enabled, zero out truncated completions for attention and loss masking if self.mask_truncated_completions: eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids_list], device=device) # Mask completion_mask for attention masking completion_mask = completion_mask * (~is_truncated).unsqueeze(1).int() # Also mask tool_mask for consistency in multi-turn training if tool_mask is not None: tool_mask = tool_mask * (~is_truncated).unsqueeze(1).int() # Concatenate prompt_mask with completion_mask for logit computation prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # (B, P+C) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size num_images = [len(img_list) for img_list in images] if images is not None else None # Get forward_kwargs for models with multimodal inputs if images is not None: prompts_text = [ apply_chat_template( {"prompt": prompt}, self.processing_class, tools=self.tools, **self.chat_template_kwargs )["prompt"] for prompt in prompts ] prompt_inputs = self.processing_class(images=images, text=prompts_text, padding=True, return_tensors="pt") prompt_inputs = super()._prepare_inputs(prompt_inputs) forward_kwargs = {k: v for k, v in prompt_inputs.items() if k not in ["input_ids", "attention_mask"]} else: forward_kwargs = {} # If token_type_ids are used, extend them with zeros for the completion part if "token_type_ids" in forward_kwargs: token_type_ids = forward_kwargs["token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and token_type_ids must have equal len padding_size = prompt_ids.size(1) - token_type_ids.size(1) if padding_size > 0: token_type_ids = torch.cat( [token_type_ids.new_zeros((token_type_ids.size(0), padding_size)), token_type_ids], dim=1 ) forward_kwargs["token_type_ids"] = torch.cat( [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # If mm_token_type_ids are used, extend them with zeros for the completion part if "mm_token_type_ids" in forward_kwargs: mm_token_type_ids = forward_kwargs["mm_token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and mm_token_type_ids must have equal len padding_size = prompt_ids.size(1) - mm_token_type_ids.size(1) if padding_size > 0: mm_token_type_ids = torch.cat( [mm_token_type_ids.new_zeros((mm_token_type_ids.size(0), padding_size)), mm_token_type_ids], dim=1, ) forward_kwargs["mm_token_type_ids"] = torch.cat( [mm_token_type_ids, mm_token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # When gradient checkpointing is enabled with use_reentrant=True (non default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): # If the generation and optimization steps are misaligned—i.e., if generation does not occur at the end of # a full optimizer step (when gradient_accumulation_steps is not a multiple of generate_every)—then the # samples may come from an earlier version of the model. In that case, we need to track old_per_token_logps # for importance sampling. If the steps are aligned, importance sampling isn't necessary and we set # old_per_token_logps to None. # When using vLLM, we always compute old_per_token_logps for importance sampling, it was shown that the # distribution mismatch between vLLM and the training model can be large and harm the training. generate_every = self.args.steps_per_generation * self.num_iterations # generation frequency if self.args.gradient_accumulation_steps % generate_every != 0 or ( self.use_vllm and self.vllm_importance_sampling_correction ): old_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: old_per_token_logps = None # Compute the importance sampling ratio when using vLLM, to correct for potential distribution mismatch if self.use_vllm and self.vllm_importance_sampling_correction: mask = completion_mask if tool_mask is None else completion_mask * tool_mask per_token_logps_diff = (old_per_token_logps - sampling_per_token_logps) * mask sequence_level_is = self.vllm_importance_sampling_mode in ["sequence_mask", "sequence_truncate"] if sequence_level_is: per_sequence_logps_diff = per_token_logps_diff.sum(dim=-1, keepdim=True) logps_diff = per_sequence_logps_diff else: logps_diff = per_token_logps_diff vllm_importance_sampling_ratio = torch.exp(logps_diff) # vllm_importance_sampling_ratio.shape: # token_* modes: (B, T) (per-token ratio) # sequence_* modes: (B, 1) (per-sequence ratio) if self.vllm_importance_sampling_mode in ["sequence_truncate", "token_truncate"]: vllm_importance_sampling_ratio = torch.clamp( vllm_importance_sampling_ratio, max=self.vllm_importance_sampling_cap ) elif self.vllm_importance_sampling_mode in ["sequence_mask", "token_mask"]: vllm_importance_sampling_ratio = vllm_importance_sampling_ratio.masked_fill( vllm_importance_sampling_ratio > self.vllm_importance_sampling_cap, value=0.0 ) else: raise ValueError( f"Unknown vLLM importance sampling level: {self.vllm_importance_sampling_mode}. Possible values are 'token_truncate', 'token_mask', 'sequence_truncate', and 'sequence_mask'." ) # Compute the per-token log probabilities for the reference model if self.beta != 0.0: if self.ref_model is not None: ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.ref_model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: # When training a PEFT adapter, how we obtain the reference depends on the setup: # - New adapter: disabling adapters yields the base model. # - Re-training an existing adapter: an initial copy is loaded under the name "ref". model = self.accelerator.unwrap_model(self.model) with use_adapter(model, adapter_name="ref" if "ref" in model.peft_config else None): ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: ref_per_token_logps = None # Decode prompts_text = self.processing_class.batch_decode(prompt_ids, skip_special_tokens=True) completions_text = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Merge extra_fields from rollout_func into inputs for reward functions if extra_fields: for i, inp in enumerate(inputs): for key, values in extra_fields.items(): if isinstance(values, list) and i < len(values): inp[key] = values[i] elif not isinstance(values, list): inp[key] = values # Calculate rewards for each reward function. rewards_per_func aggregates rewards across all processes. This is # important because rewards will be normalized per group, and completions are distributed. We will later slice # rewards_per_func to extract each process's subset. rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list) num_generations = self.num_generations if mode == "train" else self.num_generations_eval if self.multi_objective_aggregation == "sum_then_normalize": # Apply weights to each reward function's output and sum rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) mean_grouped_rewards = rewards.view(-1, num_generations).mean(dim=1) mean_grouped_rewards = mean_grouped_rewards.repeat_interleave(num_generations, dim=0) if self.scale_rewards in ["group", "none"]: # If self.scale_rewards = "none", we'll only use std_rewards to check for zero std for logging if num_generations > 1: std_rewards = rewards.view(-1, num_generations).std(dim=1) std_rewards = std_rewards.repeat_interleave(num_generations, dim=0) else: # doesn't occur during training, but could occur in eval when num_generations_eval=1 std_rewards = torch.zeros_like(rewards) elif self.scale_rewards == "batch": # Compute global std if rewards.numel() > 1: std_rewards = rewards.std().expand_as(rewards) else: # doesn't occur during training, but could occur in eval when num_generations_eval=batch_size=1 std_rewards = torch.zeros_like(rewards) else: raise ValueError( f"Invalid value for scale_rewards: {self.scale_rewards}. Must be one of 'batch', 'group', or 'none'." ) advantages = rewards - mean_grouped_rewards if self.scale_rewards != "none": advantages = advantages / (std_rewards + 1e-4) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) # for logging elif self.multi_objective_aggregation == "normalize_then_sum": grouped = rewards_per_func.view(-1, num_generations, len(self.reward_funcs)) mean_k = torch.nanmean(grouped, dim=1, keepdim=True) std_k = nanstd(grouped, dim=1, keepdim=True) if num_generations > 1 else torch.zeros_like(mean_k) reward_k = (grouped - mean_k) / (std_k + 1e-4) reward_k = reward_k.view(-1, len(self.reward_funcs)) rewards = (reward_k * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) std_rewards = rewards.std().expand_as(rewards) if rewards.numel() > 1 else torch.zeros_like(rewards) advantages = (rewards - rewards.mean()) / (std_rewards + 1e-4) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) # for logging else: raise ValueError( f"Invalid multi_objective_aggregation: {self.multi_objective_aggregation}. Must be " "'sum_then_normalize' or 'normalize_then_sum'." ) # Slice to keep only the local part of the data process_slice = slice( self.accelerator.process_index * len(prompts), (self.accelerator.process_index + 1) * len(prompts), ) all_process_advantages = advantages.clone() # keep the aggregated advantages for logging advantages = advantages[process_slice] # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) for i, reward_func_name in enumerate(self.reward_func_names): mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) std_func_rewards = nanstd(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_func_rewards) rewards = rewards_per_func.nansum(dim=1) self._metrics[mode]["reward"].append(rewards.mean().item()) self._metrics[mode]["reward_std"].append(rewards.std().item()) self._metrics[mode]["frac_reward_zero_std"].append(is_std_zero.float().mean().item()) # Log prompt and completion texts self._logs["prompt"].extend(gather_object(prompts_text)) self._logs["completion"].extend(gather_object(completions_text)) for i, name in enumerate(self.reward_func_names): self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) self._logs["advantages"].extend(all_process_advantages.tolist()) # Flush user-logged extra columns (from log_extra), gathering across processes. # Keys must be sorted so that all ranks call gather_object in the same order, otherwise values # get mis-attributed across columns (dict insertion order may differ between processes). for column in sorted(self._pending_extra_logs): self._logs["extra"][column].extend(gather_object(self._pending_extra_logs[column])) self._pending_extra_logs.clear() # Flush user-logged metrics (from log_metric), averaging across processes. # Keys must be sorted so that all ranks call accelerator.gather in the same order, otherwise values # get mis-attributed across metrics (dict insertion order may differ between processes). for name in sorted(self._pending_metrics): values = self._pending_metrics[name] local_mean = sum(values) / len(values) global_mean = self.accelerator.gather(torch.tensor(local_mean, device=device)).mean().item() self._metrics[mode][name].append(global_mean) self._pending_metrics.clear() if images is not None: self._logs["images"].extend(gather_object(images)) if self.use_vllm and self.vllm_importance_sampling_correction: delta = torch.abs(old_per_token_logps - sampling_per_token_logps) mask = completion_mask.bool() if tool_mask is None else (completion_mask * tool_mask).bool() delta = delta[mask] mean_delta = torch.mean(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) max_delta = torch.max(delta) if delta.numel() > 0 else torch.tensor(0.0, device=device) self._metrics[mode]["sampling/sampling_logp_difference/mean"].append( self.accelerator.gather(mean_delta).mean().item() ) self._metrics[mode]["sampling/sampling_logp_difference/max"].append( self.accelerator.gather(max_delta).max().item() ) if sequence_level_is: flat_is_ratio = vllm_importance_sampling_ratio.flatten() else: flat_is_ratio = vllm_importance_sampling_ratio[mask] min_importance_sampling_ratio = ( torch.min(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) mean_importance_sampling_ratio = ( torch.mean(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) max_importance_sampling_ratio = ( torch.max(flat_is_ratio) if flat_is_ratio.numel() > 0 else torch.tensor(0.0, device=device) ) self._metrics[mode]["sampling/importance_sampling_ratio/min"].append( nanmin(self.accelerator.gather(min_importance_sampling_ratio)).item() ) self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append( self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item() ) self._metrics[mode]["sampling/importance_sampling_ratio/max"].append( nanmax(self.accelerator.gather(max_importance_sampling_ratio)).item() ) output = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "advantages": advantages, "num_items_in_batch": num_items_in_batch, } if old_per_token_logps is not None: output["old_per_token_logps"] = old_per_token_logps if self.use_vllm and self.vllm_importance_sampling_correction: output["importance_sampling_ratio"] = vllm_importance_sampling_ratio if sampling_per_token_logps is not None: output["sampling_per_token_logps"] = sampling_per_token_logps if ref_per_token_logps is not None: output["ref_per_token_logps"] = ref_per_token_logps if "pixel_values" in forward_kwargs: output["pixel_values"] = forward_kwargs["pixel_values"] if "image_grid_thw" in forward_kwargs: output["image_grid_thw"] = forward_kwargs["image_grid_thw"] if "pixel_attention_mask" in forward_kwargs: output["pixel_attention_mask"] = forward_kwargs["pixel_attention_mask"] if "image_sizes" in forward_kwargs: output["image_sizes"] = forward_kwargs["image_sizes"] if "token_type_ids" in forward_kwargs: output["token_type_ids"] = forward_kwargs["token_type_ids"] if "mm_token_type_ids" in forward_kwargs: output["mm_token_type_ids"] = forward_kwargs["mm_token_type_ids"] if images is not None: output["num_images"] = num_images if tool_mask is not None: output["tool_mask"] = tool_mask return output def compute_liger_loss(self, unwrapped_model, inputs): # Compute the per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens # Get the last hidden state of the model last_hidden_state = self._get_last_hidden_state( unwrapped_model, input_ids, attention_mask, logits_to_keep, inputs.get("pixel_values"), inputs.get("image_grid_thw"), inputs.get("pixel_attention_mask"), inputs.get("image_sizes"), ) # Apply tool_mask (from env_mask) for loss computation in multi-turn training scenarios loss_mask = completion_mask if "tool_mask" not in inputs else completion_mask * inputs["tool_mask"] # Compute loss and metrics using liger grpo loss loss, metrics = self.liger_grpo_loss( _input=last_hidden_state, lin_weight=unwrapped_model.lm_head.weight, selected_token_ids=completion_ids, # The attention_mask parameter in liger loss is actually used as a loss mask (not model attention) attention_mask=loss_mask, advantages=inputs["advantages"], bias=unwrapped_model.lm_head.bias, old_per_token_logps=inputs.get("old_per_token_logps"), ref_per_token_logps=inputs.get("ref_per_token_logps"), vllm_is_ratio=inputs.get("importance_sampling_ratio"), ) # Extract metrics from the liger_grpo_loss output # KL divergence is the first metric when beta is non-zero mean_kl = metrics[0] if self.beta != 0.0 else None clip_ratio = metrics[-1] mode = "train" if self.model.training else "eval" if self.beta != 0.0: self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).mean().item()) self._metrics[mode]["clip_ratio"].append(self.accelerator.gather(clip_ratio).mean().item()) normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval return loss / normalizer @profiling_decorator def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): if return_outputs: raise ValueError("The GRPOTrainer does not support returning outputs") if self.use_liger_kernel: # Compute the loss using the liger grpo loss unwrapped_model = self.accelerator.unwrap_model(model) return self._forward_redirection(model, unwrapped_model, self.compute_liger_loss, unwrapped_model, inputs) else: return self._compute_loss(model, inputs) @staticmethod def get_off_policy_mask( advantages: torch.Tensor, per_token_logps: torch.Tensor, sampling_per_token_logps: torch.Tensor, mask: torch.Tensor, off_policy_threshold: float, ) -> torch.Tensor: """ Computes the Off-Policy Sequence Mask from DeepSeek-V3.2 paper. Returns a (B, 1) tensor where 1.0 indicates "Keep" and 0.0 indicates "Drop". """ # forward KL div: log(pi_old) - log(pi_theta) kl_div = sampling_per_token_logps - per_token_logps.detach() # Sequence-level Mean KL (ignoring prompt+padding) seq_kl_sum = (kl_div * mask).sum(dim=1, keepdim=True) avg_seq_kl = seq_kl_sum / mask.sum(dim=1, keepdim=True).clamp(min=1.0) # Keep if (Advantage >= 0) OR (KL <= delta) is_pos_adv = advantages >= 0 is_low_kl = avg_seq_kl <= off_policy_threshold return (is_pos_adv | is_low_kl).to(dtype=mask.dtype) # (B, 1) @staticmethod @torch.no_grad() def get_gamma_weights( advantages: torch.Tensor, log_ratio_per_token: torch.Tensor, mask: torch.Tensor, importance_sampling_ratio: torch.Tensor | None, # (B, T) k_pos: float = 2.0, lambda_pos: float = 3.0, k_neg: float = 3.0, lambda_neg: float = 2.0, ) -> torch.Tensor: """ Computes the Gamma weights for the VESPO loss. For reference: φ(w) = e^λ × w^k × e^{-λw} is the gamma weighting (normalized so φ(1)=1) with w = sequence-level importance sampling ratio note: we will compute φ(w) in log space φ(w) is detached via @torch.no_grad(), only acts as gradient scaling coefficient VESPO loss = -φ(w) × A × log_prob, gradient naturally gives φ(w) × A × ∇log π """ # reducing clamp range directly to log(1e-8) ~ -18.42, to avoid recomputing log_w=log(w.clamp(min=1e-8)) later # This is solely for matching truthfully the original implementation, otherwise keeping -20 could be fine. lower_clamp = math.log(1e-8) # Sequence-level log ratio Σ log(π_θ/π_old) (not a mean like for `log_importance_weights`) log_ratio_clamped = torch.clamp(log_ratio_per_token, -20.0, 20.0) seq_log_ratio = torch.sum(log_ratio_clamped * mask, dim=-1, keepdim=True) # (B, 1) # Apply token-level TIS or MIS correction (in log space) if importance_sampling_ratio is not None: log_is_ratio = torch.clamp(torch.log(importance_sampling_ratio), lower_clamp, 20.0) # log(w) = log(π_θ/π_old) + log(π_old/π_sampler) seq_log_ratio += torch.sum(log_is_ratio, dim=-1, keepdim=True) log_w_seq = torch.clamp(seq_log_ratio, lower_clamp, 20.0) w_seq = torch.exp(log_w_seq) # compute k and lambda based on advantage sign is_nonneg_adv = advantages >= 0 k_seq = torch.where(is_nonneg_adv, k_pos, k_neg) lambda_seq = torch.where(is_nonneg_adv, lambda_pos, lambda_neg).clamp(min=1e-4) # log(φ(w)) = λ + k × log(w) - λ × w log_phi = lambda_seq + k_seq * log_w_seq - lambda_seq * w_seq phi_seq = torch.exp(log_phi).nan_to_num(nan=0.0, posinf=0.0, neginf=0.0) return phi_seq # (B, 1) def _compute_loss(self, model, inputs): # Compute the per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens mask = completion_mask if "tool_mask" not in inputs else completion_mask * inputs["tool_mask"] # Compute the per_token_logps and the entropy at each position in the completion per_token_logps, entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, pixel_values=inputs.get("pixel_values"), image_grid_thw=inputs.get("image_grid_thw"), num_images=inputs.get("num_images"), pixel_attention_mask=inputs.get("pixel_attention_mask"), image_sizes=inputs.get("image_sizes"), token_type_ids=inputs.get("token_type_ids"), mm_token_type_ids=inputs.get("mm_token_type_ids"), ) if self.top_entropy_quantile < 1.0: entropy_mask = self.get_high_entropy_mask(entropies, mask, 1 - self.top_entropy_quantile) else: entropy_mask = None # Compute the loss advantages = inputs["advantages"] # In the base GRPO implementation, advantages are expected to have shape (B,). To support subclasses that # provide advantages with shape (B, T) (e.g., MiniLLM), we *conditionally* unsqueeze the tensor. if advantages.dim() == 1: advantages = advantages.unsqueeze(1) # When num_iterations == 1 and steps_per_generation <= gradient_accumulation_steps, # old_per_token_logps == per_token_logps. In this case we can skip its computation # (see _generate_and_score_completions) and instead use per_token_logps.detach(). # The exception is when using vLLM, where we always compute old_per_token_logps # for importance sampling old_per_token_logps = inputs.get("old_per_token_logps") old_per_token_logps = per_token_logps.detach() if old_per_token_logps is None else old_per_token_logps if self.off_policy_mask_threshold is not None: # OPSM should use inference-time logprobs to detect both sources of off-policyness: # 1. Drift from gradient updates (always present) # 2. Drift from training-inference mismatch (when using vLLM) # When using vLLM, prioritize sampling_per_token_logps, otherwise use old_per_token_logps sampling_per_token_logps = inputs.get("sampling_per_token_logps", old_per_token_logps) off_policy_mask = self.get_off_policy_mask( advantages=advantages, per_token_logps=per_token_logps, sampling_per_token_logps=sampling_per_token_logps, mask=mask, off_policy_threshold=self.off_policy_mask_threshold, ) log_ratio = per_token_logps - old_per_token_logps if self.importance_sampling_level == "token": log_importance_weights = log_ratio elif self.importance_sampling_level == "sequence": log_importance_weights = (log_ratio * mask).sum(-1) / mask.sum(-1).clamp(min=1.0) log_importance_weights = log_importance_weights.unsqueeze(-1) else: raise ValueError( f"Unknown importance sampling level: {self.importance_sampling_level}. Possible values are 'token' " "and 'sequence'." ) coef_1 = torch.exp(log_importance_weights) # Compute the KL divergence between the model and the reference model if self.beta != 0.0: ref_per_token_logps = inputs["ref_per_token_logps"] per_token_kl = ( torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1 ) # Importance sampling correction for the KL divergence if self.args.use_bias_correction_kl: per_token_kl = per_token_kl * coef_1 # From here, log_importance_weights (and all subsequent tensors, coef_1, coef_2, etc.) shape depends on # importance_sampling_level: "token" level: (B, T); "sequence" level: (B, 1) if self.loss_type == "cispo": clamped_ratios = torch.clamp(coef_1, max=self.epsilon_high).detach() per_token_loss = -clamped_ratios * advantages * per_token_logps elif self.loss_type in ["grpo", "bnpo", "dr_grpo", "dapo", "luspo"]: coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high) # Two-sided clipping if self.args.delta is not None: coef_1 = torch.clamp(coef_1, max=self.args.delta) per_token_loss1 = coef_1 * advantages per_token_loss2 = coef_2 * advantages per_token_loss = -torch.min(per_token_loss1, per_token_loss2) elif self.loss_type == "sapo": temperatures = torch.where(advantages > 0, self.args.sapo_temperature_pos, self.args.sapo_temperature_neg) soft_coef_1 = torch.sigmoid(temperatures * (coef_1 - 1)) * 4 / temperatures per_token_loss = -soft_coef_1 * advantages elif self.loss_type == "vespo": phi_seq = self.get_gamma_weights( advantages=advantages, log_ratio_per_token=log_ratio, mask=mask, importance_sampling_ratio=inputs.get("importance_sampling_ratio"), k_pos=self.args.vespo_k_pos, lambda_pos=self.args.vespo_lambda_pos, k_neg=self.args.vespo_k_neg, lambda_neg=self.args.vespo_lambda_neg, ) per_token_loss = -phi_seq * advantages * per_token_logps else: raise ValueError(f"Unknown loss type: {self.loss_type}") if self.off_policy_mask_threshold is not None: per_token_loss = per_token_loss * off_policy_mask if entropy_mask is not None: per_token_loss = per_token_loss * entropy_mask if self.use_vllm and self.vllm_importance_sampling_correction and self.loss_type != "vespo": per_token_loss = per_token_loss * inputs["importance_sampling_ratio"] if self.beta != 0.0: per_token_loss = per_token_loss + self.beta * per_token_kl mode = "train" if self.model.training else "eval" if self.loss_type in ["grpo", "sapo"]: loss = ((per_token_loss * mask).sum(-1) / mask.sum(-1).clamp(min=1.0)).mean() normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type == "bnpo": loss = (per_token_loss * mask).sum() / mask.sum().clamp(min=1.0) normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type == "dr_grpo": loss = (per_token_loss * mask).sum() / (per_token_loss.size(0) * self.max_completion_length) normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 # no accum in eval loss = loss / normalizer elif self.loss_type in ["cispo", "dapo", "vespo"]: normalizer = inputs["num_items_in_batch"] / self.accelerator.num_processes loss = (per_token_loss * mask).sum() / normalizer elif self.loss_type == "luspo": # Unless importance_sampling_level="token" (not recommended here), per_token_loss is expected to be (B, 1) loss = (per_token_loss * mask.sum(1, keepdim=True)).mean() normalizer = self.current_gradient_accumulation_steps if mode == "train" else 1.0 loss = loss / normalizer else: raise ValueError(f"Unknown loss type: {self.loss_type}") # Log the metrics completion_token_count = mask.sum().clamp(min=1.0) def masked_batch_mean(x): if x.shape[1] == 1: # when importance_sampling_level == "sequence" return x.mean() else: return (x * mask).sum() / completion_token_count if self.beta != 0.0: mean_kl = masked_batch_mean(per_token_kl) self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).nanmean().item()) mean_entropy = masked_batch_mean(entropies) self._metrics[mode]["entropy"].append(self.accelerator.gather(mean_entropy).nanmean().item()) if self.loss_type in ["grpo", "bnpo", "dr_grpo", "dapo", "luspo"]: # Compute the clipped probability ratios is_low_clipped = (coef_1 < 1 - self.epsilon_low) & (advantages < 0) is_high_clipped = (coef_1 > 1 + self.epsilon_high) & (advantages > 0) is_region_clipped = is_low_clipped | is_high_clipped low_clip = masked_batch_mean(is_low_clipped.float()) high_clip = masked_batch_mean(is_high_clipped.float()) clip_ratio = masked_batch_mean(is_region_clipped.float()) gathered_low_clip = self.accelerator.gather(low_clip) self._metrics[mode]["clip_ratio/low_mean"].append(gathered_low_clip.nanmean().item()) self._metrics[mode]["clip_ratio/low_min"].append(nanmin(gathered_low_clip).item()) gathered_high_clip = self.accelerator.gather(high_clip) self._metrics[mode]["clip_ratio/high_mean"].append(gathered_high_clip.nanmean().item()) self._metrics[mode]["clip_ratio/high_max"].append(nanmax(gathered_high_clip).item()) gathered_clip_ratio = self.accelerator.gather(clip_ratio) self._metrics[mode]["clip_ratio/region_mean"].append(gathered_clip_ratio.nanmean().item()) elif self.loss_type == "cispo": is_cispo_clipped = (coef_1 > self.epsilon_high) & (advantages > 0) cispo_clip_ratio = masked_batch_mean(is_cispo_clipped.float()) gathered_cispo_clip_ratio = self.accelerator.gather(cispo_clip_ratio) self._metrics[mode]["cispo_clip_ratio"].append(gathered_cispo_clip_ratio.nanmean().item()) elif self.loss_type == "vespo": gathered_phi_seq = self.accelerator.gather(phi_seq) self._metrics[mode]["vespo/phi_seq_mean"].append(gathered_phi_seq.nanmean().item()) return loss # During eval, Trainer calls prediction_step. If no labels are present in the inputs, it only runs forward and # returns logits. We override prediction_step to force compute_loss, because this trainer doesn't involve labels. def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys: list[str] | None = None): inputs = self._prepare_inputs(inputs) with torch.no_grad(): with self.compute_loss_context_manager(): loss = self.compute_loss(model, inputs) loss = loss.mean().detach() return loss, None, None def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() if self.accelerator.is_main_process and self.log_completions: if is_rich_available(): print_prompt_completions_sample( self._logs["prompt"], self._logs["completion"], self._logs["rewards"], self._logs["advantages"], self.state.global_step, self.num_completions_to_print, ) logging_backends = [] if self.args.report_to and "wandb" in self.args.report_to and wandb.run is not None: logging_backends.append(wandb) if self.args.report_to and "trackio" in self.args.report_to: logging_backends.append(trackio) table = { "step": [self.state.global_step] * len(self._logs["prompt"]), "prompt": self._logs["prompt"], "completion": self._logs["completion"], **self._logs["rewards"], **self._logs["extra"], "advantage": self._logs["advantages"], } df_base = pd.DataFrame(table) df_base.to_parquet( os.path.join( self.args.output_dir, "completions", f"completions_{self.state.global_step:05d}.parquet", ) ) images_raw = self._logs["images"] or [] for logging_backend in logging_backends: if images_raw: images = [] for image_list in self._logs["images"]: images.append([logging_backend.Image(image) for image in image_list]) df = pd.concat( [df_base, pd.Series(images, name="image")], axis=1, copy=False, ) else: df = df_base if self.log_unique_prompts: df = df.drop_duplicates(subset=["prompt"]) logging_backend.log({"completions": logging_backend.Table(dataframe=df)}) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/trainer/kto_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from dataclasses import dataclass from ..import_utils import suppress_experimental_warning with suppress_experimental_warning(): from ..experimental.kto import KTOConfig as _KTOConfig @dataclass class KTOConfig(_KTOConfig): def __post_init__(self): warnings.warn( "The `KTOConfig` is now located in `trl.experimental`. Please update your imports to " "`from trl.experimental.kto import KTOConfig`. For more information, see " "https://github.com/huggingface/trl/issues/4223. Promoting KTO to the stable API is a high-priority task. " "Until then, this current path (`from trl import KTOConfig`) will remain, but API changes may occur.", FutureWarning, stacklevel=3, ) super().__post_init__() ================================================ FILE: trl/trainer/kto_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from dataclasses import dataclass from ..import_utils import suppress_experimental_warning with suppress_experimental_warning(): from ..experimental.kto import KTOTrainer as _KTOTrainer @dataclass class KTOTrainer(_KTOTrainer): def __init__(self, *args, **kwargs): warnings.warn( "The `KTOTrainer` is now located in `trl.experimental`. Please update your imports to " "`from trl.experimental.kto import KTOTrainer`. For more information, see " "https://github.com/huggingface/trl/issues/4223. Promoting KTO to the stable API is a high-priority task. " "Until then, this current path (`from trl import KTOTrainer`) will remain, but API changes may occur.", FutureWarning, stacklevel=2, ) super().__init__(*args, **kwargs) ================================================ FILE: trl/trainer/model_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field @dataclass class ModelConfig: """ Configuration class for the models. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: model_name_or_path (`str`, *optional*): Model checkpoint for weights initialization. model_revision (`str`, *optional*, defaults to `"main"`): Specific model version to use. It can be a branch name, a tag name, or a commit id. dtype (`Literal["auto", "bfloat16", "float16", "float32"]`, *optional*, defaults to `"float32"`): Override the default `torch.dtype` and load the model under this dtype. Possible values are - `"bfloat16"`: `torch.bfloat16` - `"float16"`: `torch.float16` - `"float32"`: `torch.float32` - `"auto"`: Automatically derive the dtype from the model's weights. trust_remote_code (`bool`, *optional*, defaults to `False`): Whether to allow for custom models defined on the Hub in their own modeling files. This option should only be set to `True` for repositories you trust and in which you have read the code, as it will execute code present on the Hub on your local machine. attn_implementation (`str`, *optional*): Which attention implementation to use. More information in the [Kernels Hub Integrations Guide](kernels_hub). use_peft (`bool`, *optional*, defaults to `False`): Whether to use PEFT for training. lora_r (`int`, *optional*, defaults to `16`): LoRA R value. lora_alpha (`int`, *optional*, defaults to `32`): LoRA alpha. lora_dropout (`float`, *optional*, defaults to `0.05`): LoRA dropout. lora_target_modules (`str | list[str]`, *optional*): LoRA target modules. lora_target_parameters (`str | list[str]`, *optional*): List of target parameters for LoRA. lora_modules_to_save (`list[str]`, *optional*): Model layers to unfreeze & train. lora_task_type (`str`, *optional*, defaults to `"CAUSAL_LM"`): Task type to pass for LoRA (use `"SEQ_CLS"` for reward modeling). use_rslora (`bool`, *optional*, defaults to `False`): Whether to use Rank-Stabilized LoRA, which sets the adapter scaling factor to `lora_alpha/√r`, instead of the original default value of `lora_alpha/r`. use_dora (`bool`, *optional*, defaults to `False`): Enable [Weight-Decomposed Low-Rank Adaptation (DoRA)](https://huggingface.co/papers/2402.09353). This technique decomposes the updates of the weights into two parts, magnitude and direction. Direction is handled by normal LoRA, whereas the magnitude is handled by a separate learnable parameter. This can improve the performance of LoRA, especially at low ranks. Right now, DoRA only supports linear and Conv2D layers. DoRA introduces a bigger overhead than pure LoRA, so it is recommended to merge weights for inference. load_in_8bit (`bool`, *optional*, defaults to `False`): Whether to use 8 bit precision for the base model. Works only with LoRA. load_in_4bit (`bool`, *optional*, defaults to `False`): Whether to use 4 bit precision for the base model. Works only with LoRA. bnb_4bit_quant_type (`str`, *optional*, defaults to `"nf4"`): Quantization type (`"fp4"` or `"nf4"`). use_bnb_nested_quant (`bool`, *optional*, defaults to `False`): Whether to use nested quantization. """ model_name_or_path: str | None = field( default=None, metadata={"help": "Model checkpoint for weights initialization."}, ) model_revision: str = field( default="main", metadata={"help": "Specific model version to use. It can be a branch name, a tag name, or a commit id."}, ) dtype: str | None = field( default="float32", metadata={ "help": "Override the default `torch.dtype` and load the model under this dtype. It defaults to `'float32'`.", "choices": ["auto", "bfloat16", "float16", "float32"], }, ) trust_remote_code: bool = field( default=False, metadata={ "help": "Whether to allow for custom models defined on the Hub in their own modeling files. This option " "should only be set to `True` for repositories you trust and in which you have read the code, as it will " "execute code present on the Hub on your local machine." }, ) attn_implementation: str | None = field( default=None, metadata={ "help": "Which attention implementation to use. You can run `--attn_implementation=flash_attention_2`, in " "which case you must install this manually by running `pip install flash-attn --no-build-isolation`." }, ) use_peft: bool = field( default=False, metadata={"help": "Whether to use PEFT for training."}, ) lora_r: int = field( default=16, metadata={"help": "LoRA R value."}, ) lora_alpha: int = field( default=32, metadata={"help": "LoRA alpha."}, ) lora_dropout: float = field( default=0.05, metadata={"help": "LoRA dropout."}, ) lora_target_modules: list[str] | None = field( default=None, metadata={"help": "LoRA target modules."}, ) lora_target_parameters: list[str] | None = field( default=None, metadata={"help": "List of target parameters for LoRA."}, ) lora_modules_to_save: list[str] | None = field( default=None, metadata={"help": "Model layers to unfreeze & train."}, ) lora_task_type: str = field( default="CAUSAL_LM", metadata={"help": "Task type to pass for LoRA (use 'SEQ_CLS' for reward modeling)."}, ) use_rslora: bool = field( default=False, metadata={ "help": "Whether to use Rank-Stabilized LoRA, which sets the adapter scaling factor to `lora_alpha/√r`, " "instead of the original default value of `lora_alpha/r`." }, ) use_dora: bool = field( default=False, metadata={ "help": "Enable Weight-Decomposed Low-Rank Adaptation (DoRA). This technique decomposes the updates of " "the weights into two parts, magnitude and direction. Direction is handled by normal LoRA, whereas the " "magnitude is handled by a separate learnable parameter. This can improve the performance of LoRA, " "especially at low ranks. Right now, DoRA only supports linear and Conv2D layers. DoRA introduces a " "bigger overhead than pure LoRA, so it is recommended to merge weights for inference." }, ) load_in_8bit: bool = field( default=False, metadata={"help": "Whether to use 8 bit precision for the base model. Works only with LoRA."}, ) load_in_4bit: bool = field( default=False, metadata={"help": "Whether to use 4 bit precision for the base model. Works only with LoRA."}, ) bnb_4bit_quant_type: str = field( default="nf4", metadata={"help": "Quantization type.", "choices": ["fp4", "nf4"]}, ) use_bnb_nested_quant: bool = field( default=False, metadata={"help": "Whether to use nested quantization."}, ) bnb_4bit_quant_storage: str | None = field( default=None, metadata={"help": "Quantization storage dtype"}, ) def __post_init__(self): if self.load_in_8bit and self.load_in_4bit: raise ValueError("You can't use 8 bit and 4 bit precision at the same time") if hasattr(self.lora_target_modules, "__len__") and len(self.lora_target_modules) == 1: self.lora_target_modules = self.lora_target_modules[0] ================================================ FILE: trl/trainer/reward_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from .base_config import _BaseConfig @dataclass class RewardConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`RewardTrainer`]. This class includes only the parameters that are specific to Reward training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: > Parameters that control the model model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments for [`~transformers.AutoModelForCausalLM.from_pretrained`], used when the `model` argument of the [`RewardTrainer`] is provided as a string. If you're training a MoE architecture and want to include the load balancing/auxiliary loss as a part of the final loss, remember to set `output_router_logits=True` in this dictionary. chat_template_path (`str`, *optional*): If specified, sets the model's chat template. This can either be the path to a tokenizer (local directory or Hugging Face Hub model) or a direct path to a Jinja template file. When using a Jinja file, you must ensure that any special tokens referenced in the template are added to the tokenizer and that the model's embedding layer is resized accordingly. disable_dropout (`bool`, *optional*, defaults to `True`): Whether to disable dropout in the model. > Parameters that control the data preprocessing dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. eos_token (`str`, *optional*): Token used to indicate the end of a turn or sequence. If `None`, it defaults to `processing_class.eos_token`. pad_token (`str`, *optional*): Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that is also `None`, it falls back to `processing_class.eos_token`. max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the tokenized sequence. Samples are filtered out if either chosen or rejected sequence exceeds this value. If `None`, no filtering is applied. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. > Parameters that control the training center_rewards_coefficient (`float`, *optional*): Coefficient to incentivize the reward model to output mean-zero rewards (proposed by https://huggingface.co/papers/2312.09244, Eq. 2). Recommended value: `0.01`. activation_offloading (`bool`, *optional*, defaults to `False`): Whether to offload the activations to the CPU. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-4` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-4, metadata={"help": "The initial learning rate for AdamW."}, ) # Parameters that control the model model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments for `AutoModelForCausalLM.from_pretrained`, used when the `model` argument of " "the `RewardTrainer` is provided as a string. If you're training a MoE architecture and want to include " "the load balancing/auxiliary loss as a part of the final loss, remember to set " "`output_router_logits=True` in this dictionary." }, ) chat_template_path: str | None = field( default=None, metadata={ "help": "If specified, sets the model's chat template. This can either be the path to a tokenizer (local " "directory or Hugging Face Hub model) or a direct path to a Jinja template file. When using a Jinja file, " "you must ensure that any special tokens referenced in the template are added to the tokenizer and " "that the model's embedding layer is resized accordingly." }, ) disable_dropout: bool = field( default=True, metadata={"help": "Whether to disable dropout in the model."}, ) # Parameters that control the data preprocessing dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) eos_token: str | None = field( default=None, metadata={ "help": "Token used to indicate the end of a turn or sequence. If `None`, it defaults to `processing_class.eos_token`." }, ) pad_token: str | None = field( default=None, metadata={ "help": "Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that " "is also `None`, it falls back to `processing_class.eos_token`." }, ) max_length: int | None = field( default=1024, metadata={ "help": "Maximum length of the tokenized sequence. Sequences longer than `max_length` are truncated from " "the right. If `None`, no truncation is applied." }, ) pad_to_multiple_of: int | None = field( default=None, metadata={"help": "If set, the sequences will be padded to a multiple of this value."}, ) # Parameters that control the training center_rewards_coefficient: float | None = field( default=None, metadata={ "help": "Coefficient to incentivize the reward model to output mean-zero rewards (proposed by " "https://huggingface.co/papers/2312.09244, Eq. 2). Recommended value: `0.01`." }, ) activation_offloading: bool = field( default=False, metadata={"help": "Whether to offload the activations to the CPU."}, ) ================================================ FILE: trl/trainer/reward_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import json import logging import os import re import warnings from collections import defaultdict from collections.abc import Callable from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path from typing import Any import torch import torch.nn as nn import transformers from accelerate import PartialState from accelerate.logging import get_logger from accelerate.utils import is_peft_model from datasets import Dataset, IterableDataset from packaging.version import Version from transformers import ( AutoModelForSequenceClassification, AutoTokenizer, DataCollator, PreTrainedModel, PreTrainedTokenizerBase, TrainerCallback, set_seed, ) from transformers.data.data_collator import DataCollatorMixin from transformers.modeling_layers import GenericForSequenceClassification from transformers.trainer_utils import EvalPrediction from transformers.utils import is_peft_available from ..chat_template_utils import clone_chat_template from ..data_utils import is_conversational from ..models import get_act_offloading_ctx_manager from .base_trainer import _BaseTrainer from .reward_config import RewardConfig from .utils import create_model_from_path, disable_dropout_in_model, get_config_model_id, pad, remove_none_values if is_peft_available(): from peft import PeftConfig, PeftModel, get_peft_model logger = get_logger(__name__) # Loading a CausalLM checkpoint into AutoModelForSequenceClassification triggers harmless warnings: # - MISSING score.weight : the new seq-clf head was not in the checkpoint and is randomly initialized. # - UNEXPECTED lm_head.weight: the causal LM head is in the checkpoint but absent from seq-clf (>= 4.57.0 only). # Both are expected consequences of intentional cross-architecture loading. We suppress them to avoid # confusing users. # Old approach using logging filter (for transformers < 4.57.0) # Note: in transformers < 4.57.0, only the MISSING score.weight warning is emitted; lm_head.weight is not reported. @contextmanager def _suppress_seqcls_cross_arch_keys(logger: logging.Logger): pattern = re.compile( r"^Some weights of \S+ were not initialized from the model checkpoint at \S+ and are newly initialized: " r"\[.*\]\nYou should probably TRAIN this model on a down-stream task to be able to use it for predictions and " r"inference\.$" ) class _Filter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: return not pattern.search(record.getMessage()) f = _Filter() logger.addFilter(f) try: yield finally: logger.removeFilter(f) # New approach using scoped override (for transformers >= 4.57.0) @contextmanager def _ignore_seqcls_cross_arch_keys(): # Scoped override: ignore the expected seq-clf head key (newly added) and the causal LM head # key (present in the checkpoint but absent from seq-clf). old_missing = getattr(GenericForSequenceClassification, "_keys_to_ignore_on_load_missing", None) old_unexpected = getattr(GenericForSequenceClassification, "_keys_to_ignore_on_load_unexpected", None) merged_missing = list(old_missing) if old_missing is not None else [] if r"^score\.weight$" not in merged_missing: merged_missing.append(r"^score\.weight$") merged_unexpected = list(old_unexpected) if old_unexpected is not None else [] if r"^lm_head\." not in merged_unexpected: merged_unexpected.append(r"^lm_head\.") GenericForSequenceClassification._keys_to_ignore_on_load_missing = merged_missing GenericForSequenceClassification._keys_to_ignore_on_load_unexpected = merged_unexpected try: yield finally: GenericForSequenceClassification._keys_to_ignore_on_load_missing = old_missing GenericForSequenceClassification._keys_to_ignore_on_load_unexpected = old_unexpected # Version-aware wrapper that chooses the appropriate approach @contextmanager def suppress_seqcls_warning(): # Use the new approach for transformers >= 4.57.0, old approach for earlier versions # The old approach is needed for 4.56.2 to avoid meta tensor issues with device_map=None if Version(transformers.__version__) >= Version("4.57.0"): with _ignore_seqcls_cross_arch_keys(): yield else: # Get the transformers logger transformers_logger = logging.getLogger("transformers.modeling_utils") with _suppress_seqcls_cross_arch_keys(transformers_logger): yield def get_dataset_column_names(dataset: Dataset | IterableDataset) -> list[str]: return list(next(iter(dataset)).keys()) if dataset.column_names is None else dataset.column_names @dataclass class DataCollatorForPreference(DataCollatorMixin): """ Data collator used for preference data. Inputs are dynamically padded to the maximum length of a batch. This collator expects each example in the input list to be a dictionary containing the `"chosen_ids"` and `"rejected_ids"` keys. The collator returns a dictionary containing the following keys: - `"input_ids"`: Tensor of input IDs, padded to the maximum length of the batch. The first half of the batch corresponds to the `"chosen_ids"` and the second half to the `"rejected_ids"`. - `"attention_mask"`: Tensor of attention mask, padded to the maximum length of the batch. Optionally, the examples can contain a `"margin"` key, in which case the returned dictionary will also contain a `"margin"` key with a tensor of margins. Args: pad_token_id (`int`): Token ID to use for padding. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. return_tensors (`str`, *optional*, defaults to `"pt"`): Type of Tensor to return. Only `"pt"` is currently supported. Examples: ```python >>> from trl.trainer.reward_trainer import DataCollatorForPreference >>> collator = DataCollatorForPreference(pad_token_id=0) >>> examples = [ ... {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5]}, ... {"chosen_ids": [6, 7], "rejected_ids": [8]}, ... ] >>> collator(examples) {'input_ids': tensor([[1, 2, 3], [6, 7, 0], [4, 5, 0], [8, 0, 0]]), 'attention_mask': tensor([[1, 1, 1], [1, 1, 0], [1, 1, 0], [1, 0, 0]])} >>> examples = [ ... {"chosen_ids": [1, 2, 3], "rejected_ids": [4, 5], "margin": 0.5}, ... {"chosen_ids": [6, 7], "rejected_ids": [8], "margin": 0.0}, ... ] >>> collator(examples) {'input_ids': tensor([[1, 2, 3], [6, 7, 0], [4, 5, 0], [8, 0, 0]]), 'attention_mask': tensor([[1, 1, 1], [1, 1, 0], [1, 1, 0], [1, 0, 0]]), 'margin': tensor([0.5, 0.0])} ``` """ pad_token_id: int pad_to_multiple_of: int | None = None return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: # Convert to tensor chosen_ids = [torch.tensor(example["chosen_ids"]) for example in examples] rejected_ids = [torch.tensor(example["rejected_ids"]) for example in examples] if "margin" in examples[0]: margins = torch.tensor([example["margin"] for example in examples], dtype=torch.float) input_ids = chosen_ids + rejected_ids attention_mask = [torch.ones_like(ids) for ids in input_ids] output = {} # Pad output["input_ids"] = pad( input_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) output["attention_mask"] = pad( attention_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) if "margin" in examples[0]: output["margin"] = margins return output class RewardTrainer(_BaseTrainer): """ Trainer for Outcome-supervised Reward Models (ORM). This class is a wrapper around the [`~transformers.Trainer`] class and inherits all of its attributes and methods. Example: ```python from trl import RewardTrainer from datasets import load_dataset dataset = load_dataset("trl-lib/ultrafeedback_binarized", split="train") trainer = RewardTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=dataset, ) trainer.train() ``` Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `AutoModelForSequenceClassification.from_pretrained` with the keyword arguments in `args.model_init_kwargs`. - A sequence classification [`~transformers.PreTrainedModel`] object. - A sequence classification [`~peft.PeftModel`] object. args ([`RewardConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. data_collator ([`~transformers.DataCollator`], *optional*): Function to use to form a batch from a list of elements of the processed `train_dataset` or `eval_dataset`. Will default to [`~trainer.reward_trainer.DataCollatorForPreference`]. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. This trainer supports [preference](#preference) type (both implicit and explicit prompt). The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). The trainer also supports processed datasets (tokenized) as long as they contain `chosen_ids` and `rejected_ids` fields. eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], *optional*): Tokenizer used to process the data. If `None`, the tokenizer is loaded from the model's name with [`~transformers.AutoTokenizer.from_pretrained`]. A padding token, `processing_class.pad_token`, must be set. If the processing class has not set a padding token, `processing_class.eos_token` will be used as the default. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function that will be used to compute metrics at evaluation. Must take a [`~transformers.EvalPrediction`] and return a dictionary string to metric values. When passing [`RewardConfig`] with `batch_eval_metrics` set to `True`, your `compute_metrics` function must take a boolean `compute_result` argument. This will be triggered after the last eval batch to signal that the function needs to calculate and return the global summary statistics rather than accumulating the batch-level statistics. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. optimizer_cls_and_kwargs (`tuple[Type[torch.optim.Optimizer], Dict[str, Any]]`, *optional*): A tuple containing the optimizer class and keyword arguments to use. Overrides `optim` and `optim_args` in `args`. Incompatible with the `optimizers` argument. Unlike `optimizers`, this argument avoids the need to place model parameters on the correct devices before initializing the Trainer. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`, *optional*): A function that preprocess the logits right before caching them at each evaluation step. Must take two tensors, the logits and the labels, and return the logits once processed as desired. The modifications made by this function will be reflected in the predictions received by `compute_metrics`. Note that the labels (second parameter) will be `None` if the dataset does not have them. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. Note that if the loaded model is a causal LM, it's highly recommended to set `modules_to_save=["score"]` in the PEFT configuration to ensure that the reward head is properly trained. """ _tag_names = ["trl", "reward-trainer"] _name = "Reward" _template_file = "rm_model_card.md" def __init__( self, model: "str | PreTrainedModel | PeftModel", args: RewardConfig | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), optimizer_cls_and_kwargs: tuple[type[torch.optim.Optimizer], dict[str, Any]] | None = None, preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: "PeftConfig | None" = None, ): # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = RewardConfig(f"{model_name}-Reward") if train_dataset is None: raise ValueError("`train_dataset` is required") elif isinstance(train_dataset, IterableDataset): # IterableDataset requires dispatch_batches=False because Accelerate's dispatch mode may try to concatenate # batches from multiple processes, leading to mismatch errors. if args.accelerator_config.dispatch_batches is True: logger.warning( "You are using an `IterableDataset` for training with `dispatch_batches=True`. `dispatch_batches` " "is forced to `False` when using an `IterableDataset`. To remove this warning, unset " "`dispatch_batches` in `RewardConfig` or set it to `False`." ) args.accelerator_config.dispatch_batches = False # Model # As AutoModelForSequenceClassification.from_pretrained() will add a random head for the model, set_seed must # be done before loading the model to ensure reproducibility. set_seed(args.seed) if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model_init_kwargs["num_labels"] = 1 # the only output of the model is the reward score with suppress_seqcls_warning(): model = create_model_from_path(model, AutoModelForSequenceClassification, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the `RewardConfig`, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) # Validate that the model has num_labels = 1 (required for reward models) if getattr(model.config, "num_labels", None) != 1: raise ValueError( f"The model has `num_labels={model.config.num_labels}`, but reward models require `num_labels=1` " "to output a single scalar reward per sequence. Please instantiate your model with `num_labels=1` " "or pass a model name as a string to have it configured automatically." ) # Processing class if processing_class is None: processing_class = AutoTokenizer.from_pretrained(get_config_model_id(model.config)) # Handle pad token for processors or tokenizers if args.eos_token is not None: eos_token = args.eos_token eos_token_id = processing_class.convert_tokens_to_ids(eos_token) if eos_token_id is None: raise ValueError( f"The specified `eos_token` ('{eos_token}') is not found in the vocabulary of the given " f"`processing_class` ({processing_class.__class__.__name__}). Ensure that the `eos_token` exists " "in the vocabulary before using it as an EOS token." ) processing_class.eos_token_id = eos_token_id if args.chat_template_path is not None: if os.path.isfile(args.chat_template_path) and args.chat_template_path.endswith((".jinja", ".j2")): with open(args.chat_template_path, encoding="utf-8") as chat_template_file: processing_class.chat_template = chat_template_file.read() added_tokens = [] else: model, processing_class, added_tokens = clone_chat_template( model, processing_class, args.chat_template_path ) else: added_tokens = [] # PEFT configuration and model wrapping if peft_config is not None: if added_tokens: # Ensure that the added tokens are trainable if peft_config.trainable_token_indices is None: peft_config.trainable_token_indices = {"embed_tokens": added_tokens} elif "embed_tokens" not in peft_config.trainable_token_indices: peft_config.trainable_token_indices["embed_tokens"] = added_tokens else: peft_config.trainable_token_indices["embed_tokens"].extend(added_tokens) # Ensure that the lm_head is trainable if peft_config.modules_to_save is None or "lm_head" not in peft_config.modules_to_save: logger.warning( "Cloning chat template added new tokens to the tokenizer, but 'lm_head' is not in PEFT's " "`modules_to_save`. As a result, the model may not learn to generate outputs with these new " "tokens, leading to degraded generation quality. To fix this, add " "`modules_to_save=['lm_head']` to your PEFT configuration." ) if peft_config.modules_to_save is None: peft_config.modules_to_save = ["lm_head"] else: peft_config.modules_to_save.append("lm_head") if is_peft_available() and is_peft_model(model) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge " "and unload the existing adapter, save the resulting base model, and then pass that base model along " "with the new `peft_config` to the trainer." ) # Create PEFT model if peft_config is not None: model = get_peft_model(model, peft_config) # When using gradient checkpointing with PEFT, we need to enable input gradients. transformers.Trainer normally # handles this, but a bug currently prevents it; see https://github.com/huggingface/transformers/issues/42489 if is_peft_available() and is_peft_model(model) and args.gradient_checkpointing: model.enable_input_require_grads() # When using QLoRA, the PEFT adapter weights are converted to bf16 to follow the recommendations from the # original paper (see https://huggingface.co/papers/2305.14314, paragraph 3). Normally, this can be done by # passing `autocast_adapter_dtype=False` to `get_peft_model`, but this option is not yet supported for # quantized models. See: https://github.com/huggingface/peft/issues/2889 # Non-quantized models do not have the `is_loaded_in_{8,4}bit` attributes, whereas quantized models do if getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False): for param in model.parameters(): if param.requires_grad: param.data = param.data.to(torch.bfloat16) # Disable dropout in the model if args.disable_dropout: disable_dropout_in_model(model) # Pad token (needed for SequenceClassification models) # If not provided, use the one from the processing class or the eos token if the processing class does not have # a pad token. pad_token = args.pad_token or processing_class.pad_token or processing_class.eos_token pad_token_id = processing_class.convert_tokens_to_ids(pad_token) if pad_token_id is None: raise ValueError( f"The specified `pad_token` ('{pad_token}') is not found in the vocabulary of the given " f"`processing_class` ({processing_class.__class__.__name__}). Ensure that the `pad_token` exists " "in the vocabulary before using it as a padding token." ) model.config.pad_token_id = pad_token_id processing_class.pad_token_id = pad_token_id # Data collator if data_collator is None: data_collator = DataCollatorForPreference( pad_token_id=pad_token_id, pad_to_multiple_of=args.pad_to_multiple_of, ) # Dataset train_dataset = self._prepare_dataset(train_dataset, processing_class, args, "train") if eval_dataset is not None: if isinstance(eval_dataset, dict): eval_dataset = { key: self._prepare_dataset(dataset, processing_class, args, key) for key, dataset in eval_dataset.items() } else: eval_dataset = self._prepare_dataset(eval_dataset, processing_class, args, "eval") # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, optimizer_cls_and_kwargs=optimizer_cls_and_kwargs, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # During evaluation, Trainer calls compute_loss() only if can_return_loss is True and label_names is empty. self.can_return_loss = True self.label_names = [] # Initialize activation offloading context if self.args.activation_offloading: self.maybe_activation_offload_context = get_act_offloading_ctx_manager(model=self.model) else: self.maybe_activation_offload_context = contextlib.nullcontext() self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 # Add tags to the model self.model.add_model_tags(self._tag_names) def _prepare_dataset( self, dataset: Dataset | IterableDataset, processing_class: PreTrainedTokenizerBase, args: RewardConfig, dataset_name: str, ) -> Dataset | IterableDataset: # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from # sampled data. if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` dataset = dataset.with_transform(remove_none_values) # If the dataset is already preprocessed (tokenized), skip the processing steps. column_names = get_dataset_column_names(dataset) is_processed = "chosen_ids" in column_names and "rejected_ids" in column_names has_legacy_processed_columns = "chosen_input_ids" in column_names and "rejected_input_ids" in column_names if has_legacy_processed_columns and not is_processed: warnings.warn( "Detected legacy dataset columns `chosen_input_ids`/`rejected_input_ids`; they are deprecated and " "will not be supported in v1. Please migrate to `chosen_ids`/`rejected_ids`.", FutureWarning, stacklevel=2, ) dataset = dataset.rename_column("chosen_input_ids", "chosen_ids") dataset = dataset.rename_column("rejected_input_ids", "rejected_ids") is_processed = True # Build the kwargs for the `map` function map_kwargs = {} if isinstance(dataset, Dataset): # IterableDataset does not support num_proc map_kwargs["num_proc"] = args.dataset_num_proc with PartialState().main_process_first(): if not is_processed: # Add EOS token to the end of the sequences if needed first_example = next(iter(dataset)) if not is_conversational(first_example): if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Adding EOS to {dataset_name} dataset" def add_eos(example, eos_token): if not example["chosen"].endswith(eos_token): example["chosen"] = example["chosen"] + eos_token if "rejected" in example and not example["rejected"].endswith(eos_token): example["rejected"] = example["rejected"] + eos_token return example dataset = dataset.map( add_eos, fn_kwargs={"eos_token": processing_class.eos_token}, **map_kwargs, ) # Tokenize the dataset if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset" def tokenize_fn(example, processing_class): tools = example.get("tools") tools = json.loads(tools) if isinstance(tools, str) else tools if "prompt" in example: # explicit prompt case example["chosen"] = example["prompt"] + example["chosen"] example["rejected"] = example["prompt"] + example["rejected"] if is_conversational(example): chosen_ids = processing_class.apply_chat_template( example["chosen"], tools=tools, return_dict=True, **example.get("chat_template_kwargs", {}), )["input_ids"] rejected_ids = processing_class.apply_chat_template( example["rejected"], tools=tools, return_dict=True, **example.get("chat_template_kwargs", {}), )["input_ids"] output = {"chosen_ids": chosen_ids, "rejected_ids": rejected_ids} else: output = { "chosen_ids": processing_class(text=example["chosen"])["input_ids"], "rejected_ids": processing_class(text=example["rejected"])["input_ids"], } return output dataset = dataset.map(tokenize_fn, fn_kwargs={"processing_class": processing_class}, **map_kwargs) # Filter samples that are longer than `max_length` if args.max_length is not None: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Filtering {dataset_name} >{args.max_length} tokens" dataset = dataset.filter( lambda example: len(example["chosen_ids"]) <= args.max_length and len(example["rejected_ids"]) <= args.max_length, **map_kwargs, ) return dataset def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). if self._signature_columns is None: self._signature_columns = ["chosen_ids", "rejected_ids", "margin"] def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): mode = "train" if self.model.training else "eval" # If not set, defaults from model config and may warn since cache isn't compatible with gradient checkpointing inputs["use_cache"] = False outputs = model(**inputs) # Split the rewards into chosen and rejected rewards_chosen, rewards_rejected = torch.chunk(outputs.logits.squeeze(-1), chunks=2) # Calculate loss, optionally modulate with margin if "margin" in inputs: loss = -nn.functional.logsigmoid(rewards_chosen - rewards_rejected - inputs["margin"]).mean() else: loss = -nn.functional.logsigmoid(rewards_chosen - rewards_rejected).mean() if self.args.center_rewards_coefficient is not None: loss += self.args.center_rewards_coefficient * torch.mean((rewards_chosen + rewards_rejected) ** 2) if mode == "train": num_tokens_in_batch = self.accelerator.gather_for_metrics(inputs["attention_mask"].sum()).sum().item() self._total_train_tokens += num_tokens_in_batch self._metrics[mode]["num_tokens"] = [self._total_train_tokens] # Compute min, mean, max, accuracy and margin with torch.no_grad(): all_rewards = self.accelerator.gather(outputs.logits) self._metrics[mode]["min_reward"].append(all_rewards.min().item()) self._metrics[mode]["mean_reward"].append(all_rewards.mean().item()) self._metrics[mode]["max_reward"].append(all_rewards.max().item()) mean_accuracy = (rewards_chosen > rewards_rejected).float().mean() mean_accuracy = self.accelerator.gather_for_metrics(mean_accuracy).mean().item() self._metrics[mode]["accuracy"].append(mean_accuracy) mean_margin = (rewards_chosen - rewards_rejected).mean() mean_margin = self.accelerator.gather_for_metrics(mean_margin).mean() self._metrics[mode]["margin"].append(mean_margin.item()) return (loss, outputs) if return_outputs else loss # Override training step to add activation offloading context. def training_step(self, *args, **kwargs): with self.maybe_activation_offload_context: return super().training_step(*args, **kwargs) def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/trainer/rloo_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from dataclasses import dataclass, field from typing import Any from .base_config import _BaseConfig @dataclass class RLOOConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`RLOOTrainer`]. This class includes only the parameters that are specific to RLOO training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: > Parameters that control the model and reference model model_init_kwargs (`str`, `dict[str, Any]`, *optional*): Keyword arguments for [`~transformers.AutoModelForCausalLM.from_pretrained`], used when the `model` argument of the [`RLOOTrainer`] is provided as a string. disable_dropout (`bool`, *optional*, defaults to `False`): Whether to disable dropout in the model. This is useful for training with a reference model, as it prevents the model from generating different logprobs for the same input. > Parameters that control the data preprocessing remove_unused_columns (`bool`, *optional*, defaults to `False`): Whether to only keep the column `"prompt"` in the dataset. If you use a custom reward function that requires any column other than `"prompts"` and `"completions"`, you should keep this to `False`. num_generations (`int`, *optional*, defaults to `2`): Number of generations per prompt to sample. The effective batch size (num_processes * per_device_batch_size * gradient_accumulation_steps) must be evenly divisible by this value. num_generations_eval (`int` or `None`, *optional*): Number of generations to sample during evaluation. This allows using fewer generations during evaluation to save computation. If `None`, uses the value of `num_generations`. max_completion_length (`int` or `None`, *optional*, defaults to `256`): Maximum length of the generated completion. ds3_gather_for_generation (`bool`, *optional*, defaults to `True`): This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for generation, improving generation speed. However, disabling this option allows training models that exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option is not compatible with vLLM generation. shuffle_dataset (`bool`, *optional*, defaults to `True`): Whether to shuffle the training dataset. pad_to_multiple_of (`int`, *optional*): If set, the prompts ids and completions ids will be padded to a multiple of this value. > Parameters that control generation generation_batch_size: (`int`, *optional*): Batch size to use for generation. If `None`, it defaults to the effective training batch size: `per_device_train_batch_size * num_processes * steps_per_generation`. In other words, there is one generation batch processed per optimization step. Mutually exclusive with `steps_per_generation`. steps_per_generation: (`int`, *optional*): Number of steps per generation. If `None`, it defaults to `gradient_accumulation_steps`. Mutually exclusive with `generation_batch_size`. temperature (`float`, defaults to `1.0`): Temperature for sampling. The higher the temperature, the more random the completions. top_p (`float`, *optional*, defaults to `1.0`): Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. Set to `1.0` to consider all tokens. top_k (`int`, *optional*, defaults to `0`): Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, top-k-filtering is disabled and all tokens are considered. min_p (`float`, *optional*): Minimum token probability, which will be scaled by the probability of the most likely token. It must be a value between `0.0` and `1.0`. Typical values are in the `0.01-0.2` range. generation_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to [`~transformers.GenerationConfig`] (if using transformers) or `SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them. chat_template_kwargs (`dict[str, Any]`, *optional*): Additional keyword arguments to pass to the `apply_chat_template` function when generating completions. repetition_penalty (`float`, *optional*, defaults to `1.0`): Float that penalizes new tokens based on whether they appear in the prompt and the generated text so far. Values > `1.0` encourage the model to use new tokens, while values < `1.0` encourage the model to repeat tokens. use_transformers_paged (`bool`, *optional*, defaults to `False`): Whether to use the `transformers` paged implementation for generation. If set to `True`, the `transformers` paged implementation will be used for generation instead of the default padded implementation. This parameter is only effective when `use_vllm` is set to `False`. cache_implementation (`str`, *optional*): Implementation of the cache method for faster generation when `use_vllm` is set to `False`. > Parameters that control generation acceleration powered by vLLM use_vllm (`bool`, *optional*, defaults to `False`): Whether to use vLLM for generating completions. If set to `True`, the trainer will use vLLM for generation instead of the default model.generate(). Requires `vllm` to be installed. vllm_mode (`str`, *optional*, defaults to `"colocate"`): Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `"server"` or `"colocate"`. - `"server"`: The trainer will send generation requests to a separate vLLM server. Make sure a TRL vLLM server is running (start with `trl vllm-serve`). - `"colocate"`: vLLM will run in the same process and share the training GPUs. This avoids the need for a separate server but may cause resource contention with training. vllm_model_impl (`str`, *optional*, defaults to `"vllm"`): Model implementation to use for vLLM. Must be one of `"transformers"` or `"vllm"`. `"transformers"`: Use the `transformers` backend for model implementation. `"vllm"`: Use the `vllm` library for model implementation. vllm_structured_outputs_regex (`str`, *optional*): Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled. > Parameters that control the vLLM server (only used when `vllm_mode` is `"server"`) vllm_server_base_url (`str`, *optional*): Base URL for the vLLM server (e.g., `"http://localhost:8000"`). If provided, `vllm_server_host` and `vllm_server_port` are ignored. vllm_server_host (`str`, *optional*, defaults to `"0.0.0.0"`): Host of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_port (`int`, *optional*, defaults to `8000`): Port of the vLLM server to connect to. Ignored if `vllm_server_base_url` is provided. vllm_server_timeout (`float`, *optional*, defaults to `240.0`): Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up after the timeout, a `ConnectionError` is raised. vllm_group_port (`int`, *optional*, defaults to `51216`): Port number for the weight update group. This is used to communicate with the vLLM server. Unless the port is occupied, there is no need to change it. > Parameters that control colocated vLLM execution (only used when `vllm_mode` is `"colocate"`) vllm_gpu_memory_utilization (`float`, *optional*, defaults to `0.3`): Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_gpu_memory_utilization` flag. vllm_max_model_length (`int`, *optional*): Context window for vLLM. Set it to at least the maximum prompt length in the dataset plus `max_completion_length`; if omitted, it is inferred from the model config. vllm_tensor_parallel_size (`int`, *optional*, defaults to `1`): Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set to `"colocate"`. If you are using `vllm_mode="server"`, this parameter must be passed separately when launching the vLLM server via the `--vllm_tensor_parallel_size` flag. vllm_enable_sleep_mode (`bool`, *optional*, defaults to `False`): Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory usage low, but waking the engine adds host–device transfer latency. > Parameters that control the training beta (`float`, *optional*, defaults to `0.05`): KL coefficient. If `0.0`, the reference model is not loaded, reducing memory usage and improving training speed. num_iterations (`int`, *optional*, defaults to `1`): Number of iterations per batch (denoted as μ in the algorithm). epsilon (`float`, *optional*, defaults to `0.2`): Epsilon value for clipping. epsilon_high (`float`, *optional*): Upper-bound epsilon value for clipping. If not specified, it defaults to the same value as the lower-bound specified in argument `epsilon`. Paper [DAPO](https://huggingface.co/papers/2503.14476) recommends `0.28`. reward_weights (`list[float]`, *optional*): Weights for each reward function. Must match the number of reward functions. If `None`, all rewards are weighted equally with weight `1.0`. normalize_advantages (`bool`, *optional*, defaults to `False`): Whether to normalize advantages. Normalization is done per generation batch to have mean `0.0` and standard deviation of `1.0`. reward_clip_range (`tuple[float, float]`, *optional*): Clip range for rewards as (min, max). If `None`, no clipping is applied. mask_truncated_completions (`bool`, *optional*, defaults to `False`): When enabled, truncated completions are excluded from the loss calculation, preventing them from being incorrectly penalized and introducing noise during training. According to the [DAPO](https://huggingface.co/papers/2503.14476) paper, this is a good practice for training stability. sync_ref_model (`bool`, *optional*, defaults to `False`): Whether to synchronize the reference model with the active model every `ref_model_sync_steps` steps, using the `ref_model_mixup_alpha` parameter. This synchronization originates from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper. ref_model_mixup_alpha (`float`, *optional*, defaults to `0.6`): α parameter from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper, which controls the mix between the current policy and the previous reference policy during updates. The reference policy is updated according to the equation: `π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`. ref_model_sync_steps (`int`, *optional*, defaults to `512`): τ parameter from the [TR-DPO](https://huggingface.co/papers/2404.09656) paper, which determines how frequently the current policy is synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`. > Parameters that control the logging log_completions (`bool`, *optional*, defaults to `False`): Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is installed, it prints the sample. If `wandb` and/or `trackio` logging is enabled, it logs it to `wandb` and/or `trackio`. num_completions_to_print (`int`, *optional*): Number of completions to print with `rich`. If `None`, all completions are logged. log_unique_prompts (`bool`, *optional*, defaults to `False`): Whether to log unique prompts. If `True`, only unique prompts are logged. If `False`, all prompts are logged. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `1e-6` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=1e-6, metadata={"help": "The initial learning rate for AdamW."}, ) # Parameters that control the model and reference model model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments for `transformers.AutoModelForCausalLM.from_pretrained`, used when the `model` " "argument of the `RLOOTrainer` is provided as a string." }, ) disable_dropout: bool = field( default=False, metadata={ "help": "Whether to disable dropout in the model. This is useful for training with a reference model, as " "it prevents the model from generating different logprobs for the same input." }, ) # Parameters that control the data preprocessing # The default value remove_unused_columns is overwritten from the parent class, because in RLOO we usually rely on # additional columns to compute the reward remove_unused_columns: bool | None = field( default=False, metadata={ "help": "Whether to only keep the column 'prompt' in the dataset. If you use a custom reward function " "that requires any column other than 'prompts' and 'completions', you should keep this to `False`." }, ) num_generations: int | None = field( default=2, metadata={ "help": "Number of generations to sample. The effective batch size (num_processes * per_device_batch_size " "* gradient_accumulation_steps) must be evenly divisible by this value." }, ) num_generations_eval: int | None = field( default=None, metadata={ "help": "Number of generations to sample during evaluation. This allows using fewer generations during " "evaluation to save computation. If `None`, uses the value of `num_generations`." }, ) max_completion_length: int | None = field( default=256, metadata={"help": "Maximum length of the generated completion."}, ) ds3_gather_for_generation: bool = field( default=True, metadata={ "help": "This setting applies to DeepSpeed ZeRO-3. If enabled, the policy model weights are gathered for " "generation, improving generation speed. However, disabling this option allows training models that " "exceed the VRAM capacity of a single GPU, albeit at the cost of slower generation. Disabling this option " "is not compatible with vLLM generation." }, ) shuffle_dataset: bool | None = field( default=True, metadata={"help": "Whether to shuffle the training dataset."}, ) pad_to_multiple_of: int | None = field( default=None, metadata={"help": "If set, the prompts ids and completions ids will be padded to a multiple of this value."}, ) # Parameters that control generation generation_batch_size: int | None = field( default=None, metadata={ "help": "Batch size to use for generation. If `None`, it defaults to the effective training batch size: " "`per_device_train_batch_size * num_processes * steps_per_generation`." }, ) steps_per_generation: int | None = field( default=None, metadata={"help": "Number of steps per generation. If `None`, it defaults to `gradient_accumulation_steps`."}, ) temperature: float = field( default=1.0, metadata={"help": "Temperature for sampling. The higher the temperature, the more random the completions."}, ) top_p: float = field( default=1.0, metadata={ "help": "Float that controls the cumulative probability of the top tokens to consider. Must be in (0, 1]. " "Set to 1.0 to consider all tokens." }, ) top_k: int = field( default=0, metadata={ "help": "Number of highest probability vocabulary tokens to keep for top-k-filtering. If `0`, " "top-k-filtering is disabled and all tokens are considered." }, ) min_p: float | None = field( default=None, metadata={ "help": "Minimum token probability, which will be scaled by the probability of the most likely token. It " "must be a value between 0.0 and 1.0. Typical values are in the 0.01-0.2 range." }, ) generation_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to `GenerationConfig` (if using transformers) or " "`SamplingParams` (if using vLLM) when sampling completions. This can be used to further customize the " "generation behavior, such as setting `suppress_tokens`, `num_beams`, etc. If it contains keys that " "conflict with the other generation parameters (like `min_p`, `top_p`, etc.), they will override them." }, ) chat_template_kwargs: dict | None = field( default=None, metadata={ "help": "Additional keyword arguments to pass to the `apply_chat_template` function when generating " "completions." }, ) repetition_penalty: float = field( default=1.0, metadata={ "help": "Float that penalizes new tokens based on whether they appear in the prompt and the generated " "text so far. Values > 1.0 encourage the model to use new tokens, while values < 1.0 encourage the model " "to repeat tokens." }, ) use_transformers_paged: bool = field( default=False, metadata={ "help": "Whether to use the `transformers` paged implementation for generation. If set to `True`, the " "`transformers` paged implementation will be used for generation instead of the default padded " "implementation. This parameter is only effective when `use_vllm` is set to `False`." }, ) cache_implementation: str | None = field( default=None, metadata={"help": "Implementation of the cache method for faster generation when use_vllm is set to False."}, ) # Parameters that control generation acceleration powered by vLLM use_vllm: bool = field( default=False, metadata={ "help": "Whether to use vLLM for generating completions. If set to `True`, the trainer will use vLLM for " "generation instead of the default model.generate(). Requires `vllm` to be installed." }, ) vllm_mode: str = field( default="colocate", metadata={ "help": "Mode to use for vLLM integration when `use_vllm` is set to `True`. Must be one of `'server'` or " "`'colocate'`. `'server'`: The trainer will send generation requests to a separate vLLM server. Make sure " "a TRL vLLM server is running (start with `trl vllm-serve`). `'colocate'`: vLLM will run in the same " "process and share the training GPUs. This avoids the need for a separate server but may cause resource " "contention with training." }, ) vllm_model_impl: str = field( default="vllm", metadata={ "help": "Model implementation to use for vLLM. Must be one of `transformers` or `vllm`. `transformers`: " "Use the `transformers` backend for model implementation. `vllm`: Use the `vllm` library for " "model implementation." }, ) vllm_enable_sleep_mode: bool = field( default=False, metadata={ "help": "Enable vLLM sleep mode to offload weights/cache during the optimizer step. Keeps GPU memory " "usage low, but waking the engine adds host–device transfer latency." }, ) vllm_structured_outputs_regex: str | None = field( default=None, metadata={"help": "Regex for vLLM structured outputs. If `None` (default), structured outputs is disabled."}, ) # Parameters that control the vLLM server (only used when `vllm_mode` is `"server"`) vllm_server_base_url: str | None = field( default=None, metadata={ "help": "Base URL for the vLLM server (e.g., 'http://localhost:8000'). If provided, `vllm_server_host` " "and `vllm_server_port` are ignored." }, ) vllm_server_host: str = field( default="0.0.0.0", metadata={"help": "Host of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_port: int = field( default=8000, metadata={"help": "Port of the vLLM server to connect to. Ignored if vllm_server_base_url is provided."}, ) vllm_server_timeout: float = field( default=240.0, metadata={ "help": "Total timeout duration in seconds to wait for the vLLM server to be up. If the server is not up " "after the timeout, a `ConnectionError` is raised." }, ) vllm_group_port: int = field( default=51216, metadata={ "help": "Port number for the weight update group. This is used to communicate with the vLLM server. " "Unless the port is occupied, there is no need to change it.", }, ) # Parameters that control colocated vLLM execution (only used when `vllm_mode` is `"colocate"`) vllm_gpu_memory_utilization: float = field( default=0.3, metadata={ "help": "Control the GPU memory utilization for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_gpu_memory_utilization` flag." }, ) vllm_max_model_length: int | None = field( default=None, metadata={ "help": "Context window for vLLM. Set it to at least the maximum prompt length in the dataset plus " "`max_completion_length`; if omitted, it is inferred from the model config." }, ) vllm_tensor_parallel_size: int = field( default=1, metadata={ "help": "Control the tensor parallel size for vLLM. This setting only applies when `vllm_mode` is set " "to `'colocate'`. If you are using `vllm_mode='server'`, this parameter must be passed separately when " "launching the vLLM server via the `--vllm_tensor_parallel_size` flag." }, ) # Parameters that control the training beta: float = field( default=0.05, metadata={ "help": "KL coefficient. If `0.0`, the reference model is not loaded, reducing memory usage and improving " "training speed." }, ) num_iterations: int = field( default=1, metadata={"help": "Number of iterations per batch (denoted as μ in the algorithm)."}, ) epsilon: float = field( default=0.2, metadata={"help": "Epsilon value for clipping."}, ) epsilon_high: float | None = field( default=None, metadata={ "help": "Upper-bound epsilon value for clipping. If not specified, it defaults to the same value as the " "lower-bound specified in argument `epsilon`. Paper DAPO recommends `0.28`." }, ) reward_weights: list[float] | None = field( default=None, metadata={ "help": "Weights for each reward function. Must match the number of reward functions. If `None`, all " "rewards are weighted equally with weight `1.0`." }, ) normalize_advantages: bool = field( default=False, metadata={ "help": "Whether to normalize advantages. Normalization is done per generation batch to have mean `0.0` " "and standard deviation of `1.0`." }, ) reward_clip_range: tuple[float, float] | None = field( default=None, metadata={"help": "Clip range for rewards as (min, max). If None, no clipping is applied."}, ) mask_truncated_completions: bool = field( default=False, metadata={ "help": "When enabled, truncated completions are excluded from the loss calculation, preventing them from " "being incorrectly penalized and introducing noise during training. According to the DAPO paper, this is " "a good practice for training stability." }, ) sync_ref_model: bool = field( default=False, metadata={ "help": "Whether to synchronize the reference model with the active model every `ref_model_sync_steps` " "steps, using the `ref_model_mixup_alpha` parameter." }, ) ref_model_mixup_alpha: float = field( default=0.6, metadata={ "help": "α parameter from the TR-DPO paper, which controls the mix between the current policy and the " "previous reference policy during updates. The reference policy is updated according to the equation: " "`π_ref = α * π_θ + (1 - α) * π_ref_prev`. To use this parameter, you must set `sync_ref_model=True`." }, ) ref_model_sync_steps: int = field( default=512, metadata={ "help": "τ parameter from the TR-DPO paper, which determines how frequently the current policy is " "synchronized with the reference policy. To use this parameter, you must set `sync_ref_model=True`." }, ) # Parameters that control the logging log_completions: bool = field( default=False, metadata={ "help": "Whether to log a sample of (prompt, completion) pairs every `logging_steps` steps. If `rich` is " "installed, it prints the sample. If `wandb` logging is enabled, it logs it to `wandb`." }, ) num_completions_to_print: int | None = field( default=None, metadata={"help": "Number of completions to print with `rich`. If `None`, all completions are logged."}, ) log_unique_prompts: bool = field( default=False, metadata={ "help": "Whether to log unique prompts. If `True`, only unique prompts are logged. If `False`, all " "prompts are logged." }, ) def __post_init__(self): super().__post_init__() num_processes = self.world_size # The current default effective batch size if self.generation_batch_size is None and self.steps_per_generation is None: self.steps_per_generation = self.gradient_accumulation_steps self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation elif self.generation_batch_size is not None and self.steps_per_generation is None: # Just ensure the value is divisible by the global batch size if self.generation_batch_size % (self.per_device_train_batch_size * num_processes) != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by the global batch size " f"({self.per_device_train_batch_size * num_processes})." ) self.steps_per_generation = self.generation_batch_size // ( self.per_device_train_batch_size * num_processes ) elif self.generation_batch_size is None and self.steps_per_generation is not None: self.generation_batch_size = self.per_device_train_batch_size * num_processes * self.steps_per_generation else: raise ValueError( "'generation_batch_size' and 'steps_per_generation' can not be both configured at the same time" ) if self.do_eval and self.eval_strategy != "no": # Determine the number of generations to use for evaluation num_generations = self.num_generations_eval or self.num_generations # Just ensure the value is divisible by the global batch size if (self.per_device_eval_batch_size * num_processes) % num_generations != 0: raise ValueError( f"The global eval batch size ({self.per_device_eval_batch_size} * {num_processes}) must be " f"divisible by the number of generations used for evaluation ({num_generations})." ) # The generation batch must contain full prompt groups (no partials), so it must be divisible by # num_generations. if self.generation_batch_size % self.num_generations != 0: raise ValueError( f"generation_batch_size ({self.generation_batch_size}) must be divisible by num_generations " f"({self.num_generations})." ) if self.num_generations < 2: raise ValueError( "RLOO requires at least 2 generations per prompt to calculate the advantages. You provided " f"{self.num_generations}, which is less than the minimum required." ) ================================================ FILE: trl/trainer/rloo_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import atexit import copy import inspect import textwrap import time from collections import defaultdict, deque from collections.abc import Callable from contextlib import nullcontext from pathlib import Path from typing import Any import numpy as np import pandas as pd import torch import torch.utils.data import transformers from accelerate.logging import get_logger from accelerate.utils import gather, gather_object, is_peft_model, set_seed from datasets import Dataset, IterableDataset from packaging.version import Version from torch import nn from torch.distributed.fsdp import FullyShardedDataParallel as FSDP from torch.utils.data import Sampler from transformers import ( AutoModelForSequenceClassification, AutoProcessor, AutoTokenizer, GenerationConfig, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainerCallback, is_trackio_available, is_wandb_available, ) from transformers.utils import is_peft_available, is_rich_available from ..data_utils import apply_chat_template, is_conversational, prepare_multimodal_messages from ..extras.profiling import profiling_context, profiling_decorator from ..generation.vllm_generation import VLLMGeneration from ..models import prepare_deepspeed, prepare_fsdp, unwrap_model_for_generation from ..models.utils import disable_gradient_checkpointing from .base_trainer import _BaseTrainer from .callbacks import SyncRefModelCallback from .rloo_config import RLOOConfig from .utils import ( RepeatSampler, create_model_from_path, disable_dropout_in_model, entropy_from_logits, get_config_model_id, identity, nanmax, nanmin, nanstd, pad, print_prompt_completions_sample, selective_log_softmax, shuffle_sequence_dict, shutdown_event_loop_in_daemon, split_pixel_values_by_grid, split_tensor_dict, start_event_loop_in_daemon, unsplit_pixel_values_by_grid, use_adapter, ) if is_peft_available(): from peft import PeftConfig, PeftModel, get_peft_model if is_wandb_available(): import wandb if is_trackio_available(): import trackio logger = get_logger(__name__) # A reward function can be a string, interpreted as a model ID and loaded as a pretrained model, a pretrained model, or # a callable that returns a list of floats (the rewards). The callable receives prompts, completions, and additional # arguments from the trainer (refer to the trainer's source for details). To ensure forward compatibility, it should # accept **kwargs. RewardFunc = str | PreTrainedModel | Callable[..., list[float | None]] class RLOOTrainer(_BaseTrainer): """ Trainer for the Reinforce Leave One Out (RLOO) method. This algorithm was initially proposed in the paper [Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs](https://huggingface.co/papers/2402.14740). Example: ```python from trl import RLOOTrainer from trl.rewards import accuracy_reward from datasets import load_dataset dataset = load_dataset("trl-lib/DeepMath-103K", split="train") trainer = RLOOTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", reward_funcs=accuracy_reward, train_dataset=dataset, ) trainer.train() ``` Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `.from_pretrained` (where `` is derived from the model config) with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. - A [`~peft.PeftModel`] object. Only causal language models are supported. reward_funcs (`RewardFunc | list[RewardFunc]`): Reward functions to be used for computing the rewards. To compute the rewards, we call all the reward functions with the prompts and completions and sum the rewards. Can be either: - A single reward function, such as: - A string: The *model ID* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using [`~transformers.AutoModelForSequenceClassification.from_pretrained`] with `num_labels=1` and the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object: Only sequence classification models are supported. - A custom reward function: The function is provided with the prompts and the generated completions, plus any additional columns in the dataset. It should return a list of rewards. Custom reward functions can be either synchronous or asynchronous and can also return `None` when the reward is not applicable to those samples. This is useful for multi-task training where different reward functions apply to different types of samples. When a reward function returns `None` for a sample, that reward function is excluded from the reward calculation for that sample. For more details, see [Using a custom reward function](#using-a-custom-reward-function). The trainer's state is also passed to the reward function. The trainer's state is an instance of [`~transformers.TrainerState`] and can be accessed by accessing the `trainer_state` argument to the reward function's signature. - A list of reward functions, where each item can independently be any of the above types. Mixing different types within the list (e.g., a string model ID and a custom reward function) is allowed. args ([`RLOOConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. It must include a column `"prompt"`. Any additional columns in the dataset is ignored. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. The padding side must be set to "left". If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. reward_processing_classes ([`~transformers.PreTrainedTokenizerBase`] or `list[PreTrainedTokenizerBase]`, *optional*): Processing classes corresponding to the reward functions specified in `reward_funcs`. Can be either: - A single processing class: Used when `reward_funcs` contains only one reward function. - A list of processing classes: Must match the order and length of the reward functions in `reward_funcs`. If set to `None`, or if an element of the list corresponding to a [`~transformers.PreTrainedModel`] is `None`, the tokenizer for the model is automatically loaded using [`~transformers.AutoTokenizer.from_pretrained`]. For elements in `reward_funcs` that are custom reward functions (not [`~transformers.PreTrainedModel`]), the corresponding entries in `reward_processing_classes` are ignored. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. """ _tag_names = ["trl", "rloo"] _name = "RLOO" _paper = { "title": "Back to Basics: Revisiting REINFORCE-Style Optimization for Learning from Human Feedback in LLMs", "id": "2402.14740", # docstyle-ignore "citation": textwrap.dedent("""\ @inproceedings{ahmadian2024back, title = {{Back to Basics: Revisiting REINFORCE-Style Optimization for Learning from Human Feedback in LLMs}}, author = {Arash Ahmadian and Chris Cremer and Matthias Gall{\'{e}} and Marzieh Fadaee and Julia Kreutzer and Olivier Pietquin and Ahmet {\"{U}}st{\"{u}}n and Sara Hooker}, year = 2024, booktitle = {Proceedings of the 62nd Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), {ACL} 2024, Bangkok, Thailand, August 11-16, 2024}, pages = {12248--12267}, publisher = {Association for Computational Linguistics}, editor = {Lun{-}Wei Ku and Andre Martins and Vivek Srikumar}, }"""), } def __init__( self, model: "str | PreTrainedModel | PeftModel", reward_funcs: RewardFunc | list[RewardFunc], args: RLOOConfig | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, reward_processing_classes: PreTrainedTokenizerBase | list[PreTrainedTokenizerBase] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), peft_config: "PeftConfig | None" = None, ): # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = RLOOConfig(f"{model_name}-RLOO") # Model if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model = create_model_from_path(model, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the `RLOOConfig`, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) # Some models (SmolVLM/Idefics3) don't support `logits_to_keep` argument and error out if we pass it # Inspect the forward method before we wrap the model with PEFT self.model_kwarg_keys = ( inspect.signature(model.forward).parameters.keys() if not hasattr(model, "get_base_model") else inspect.signature(model.get_base_model().forward).parameters.keys() ) # Processing class if processing_class is None: processing_class = AutoProcessor.from_pretrained( get_config_model_id(model.config), truncation_side="left", padding_side="left" ) # Handle pad token for processors or tokenizers if isinstance(processing_class, ProcessorMixin): tokenizer = processing_class.tokenizer elif isinstance(processing_class, PreTrainedTokenizerBase): tokenizer = processing_class else: raise TypeError("The `processing_class` must be either a `PreTrainedTokenizerBase` or a `ProcessorMixin`") if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token self.pad_token = tokenizer.pad_token self.pad_token_id = tokenizer.pad_token_id self.eos_token_id = tokenizer.eos_token_id if is_peft_available() and is_peft_model(model) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge " "and unload the existing adapter, save the resulting base model, and then pass that base model along " "with the new `peft_config` to the trainer." ) if is_peft_available() and is_peft_model(model): # If the model is a PEFT model with a pretrained adapter, we need to create a "ref" adapter that is a copy # of the "default" adapter, so that we can use it as the reference model during the training. model.add_adapter("ref", model.peft_config["default"]) for name, param in model.named_parameters(): if ".default." in name: ref_name = name.replace(".default.", ".ref.") ref_param = model.get_parameter(ref_name) ref_param.data.copy_(param.data) # Create PEFT model if peft_config is not None: model = get_peft_model(model, peft_config) # When using gradient checkpointing with PEFT, we need to enable input gradients. transformers.Trainer normally # handles this, but a bug currently prevents it; see https://github.com/huggingface/transformers/issues/42489 if is_peft_available() and is_peft_model(model) and args.gradient_checkpointing: model.enable_input_require_grads() # When using QLoRA, the PEFT adapter weights are converted to bf16 to follow the recommendations from the # original paper (see https://huggingface.co/papers/2305.14314, paragraph 3). Normally, this can be done by # passing `autocast_adapter_dtype=False` to `get_peft_model`, but this option is not yet supported for # quantized models. See: https://github.com/huggingface/peft/issues/2889 # Non-quantized models do not have the `is_loaded_in_{8,4}bit` attributes, whereas quantized models do if getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False): for param in model.parameters(): if param.requires_grad: param.data = param.data.to(torch.bfloat16) # Reward functions if not isinstance(reward_funcs, list): reward_funcs = [reward_funcs] self.reward_func_names = [] for i, reward_func in enumerate(reward_funcs): if isinstance(reward_func, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None reward_funcs[i] = AutoModelForSequenceClassification.from_pretrained( reward_func, num_labels=1, **model_init_kwargs ) if isinstance(reward_funcs[i], nn.Module): # Use Module over PretrainedModel for compat w/ compiled models self.reward_func_names.append(get_config_model_id(reward_funcs[i].config).split("/")[-1]) else: self.reward_func_names.append(reward_funcs[i].__name__) self.reward_funcs = reward_funcs self._has_async_reward_funcs = any(inspect.iscoroutinefunction(func) for func in self.reward_funcs) if self._has_async_reward_funcs: self.async_reward_loop_thread, self.async_reward_loop, self.async_reward_loop_ready_event = ( start_event_loop_in_daemon(name="RLOOTrainer-AsyncRewardLoop") ) # wait until the event loop is running in the daemon thread self.async_reward_loop_ready_event.wait() atexit.register(shutdown_event_loop_in_daemon, self.async_reward_loop_thread, self.async_reward_loop) # Reward weights if args.reward_weights is not None: if len(args.reward_weights) != len(reward_funcs): raise ValueError( f"Number of reward weights ({len(args.reward_weights)}) must match number of reward " f"functions ({len(reward_funcs)})" ) self.reward_weights = torch.tensor(args.reward_weights, dtype=torch.float32) else: self.reward_weights = torch.ones(len(reward_funcs), dtype=torch.float32) # Reward processing class if reward_processing_classes is None: reward_processing_classes = [None] * len(reward_funcs) elif not isinstance(reward_processing_classes, list): reward_processing_classes = [reward_processing_classes] if len(reward_processing_classes) != len(reward_funcs): raise ValueError( f"The number of reward processing classes ({len(reward_processing_classes)}) must match the number of " f"reward functions ({len(reward_funcs)})." ) for i, (reward_processing_class, reward_func) in enumerate( zip(reward_processing_classes, reward_funcs, strict=True) ): if isinstance(reward_func, PreTrainedModel): if reward_processing_class is None: reward_processing_class = AutoTokenizer.from_pretrained(get_config_model_id(reward_func.config)) if reward_processing_class.pad_token_id is None: reward_processing_class.pad_token = reward_processing_class.eos_token # The reward model computes the reward for the latest non-padded token in the input sequence. # So it's important to set the pad token ID to the padding token ID of the processing class. reward_func.config.pad_token_id = reward_processing_class.pad_token_id reward_processing_classes[i] = reward_processing_class self.reward_processing_classes = reward_processing_classes # Training arguments self.max_completion_length = args.max_completion_length self.num_generations = args.num_generations self.num_generations_eval = args.num_generations_eval or self.num_generations self.chat_template_kwargs = args.chat_template_kwargs or {} self.temperature = args.temperature self.top_p = args.top_p self.top_k = args.top_k self.min_p = args.min_p self.repetition_penalty = args.repetition_penalty self.use_transformers_paged = args.use_transformers_paged self.pad_to_multiple_of = args.pad_to_multiple_of self.use_vllm = args.use_vllm self.vllm_mode = args.vllm_mode self.vllm_gpu_memory_utilization = args.vllm_gpu_memory_utilization # only applies to colocation mode self.vllm_tensor_parallel_size = args.vllm_tensor_parallel_size # only applies to colocation mode self.normalize_advantages = args.normalize_advantages self.mask_truncated_completions = args.mask_truncated_completions self.reward_clip_range = args.reward_clip_range # Datasets self.shuffle_dataset = args.shuffle_dataset if train_dataset is None: raise ValueError("`train_dataset` is required") elif ( isinstance(train_dataset, IterableDataset) or isinstance(eval_dataset, IterableDataset) or ( isinstance(eval_dataset, dict) and any(isinstance(ds, IterableDataset) for ds in eval_dataset.values()) ) ): # See https://github.com/huggingface/trl/issues/3213 raise NotImplementedError( "Iterable datasets are not yet supported in RLOOTrainer. Please use a standard dataset instead." ) # Multi-step self.num_iterations = args.num_iterations self.epsilon_low = args.epsilon self.epsilon_high = args.epsilon_high if args.epsilon_high is not None else args.epsilon # Tracks the number of iterations (forward + backward passes), including those within a grad accum cycle self._step = 0 # Buffer the batch to reuse generated outputs across multiple updates. For more details, see # `_get_train_sampler` and `_prepare_inputs`. self._buffered_inputs = None # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=identity, # No data collation is needed in RLOO train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, callbacks=callbacks, optimizers=optimizers, ) # Reference model self.beta = args.beta if self.beta == 0.0: # If beta is 0.0, the reference model is not needed self.ref_model = None elif is_peft_model(model): # If PEFT is used, the reference model is not needed since the adapter can be disabled # to revert to the initial model. self.ref_model = None else: # For deepspeed, fsdp or non-distributed models, create a reference model from scratch model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if self.args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None self.ref_model = create_model_from_path(get_config_model_id(self.model.config), **model_init_kwargs) # Disable dropout in the models if args.disable_dropout: disable_dropout_in_model(model) if self.ref_model is not None: disable_dropout_in_model(self.ref_model) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 self._current_train_step_time = 0.0 self.log_completions = args.log_completions self.log_unique_prompts = args.log_unique_prompts self.num_completions_to_print = args.num_completions_to_print # Keep logs sized to the generation batch to record only outputs from the latest model update. self._logs = { "images": deque(maxlen=args.generation_batch_size), "prompt": deque(maxlen=args.generation_batch_size), "completion": deque(maxlen=args.generation_batch_size), "rewards": defaultdict(lambda: deque(maxlen=args.generation_batch_size)), "advantages": deque(maxlen=args.generation_batch_size), "extra": defaultdict(lambda: deque(maxlen=args.generation_batch_size)), } # Buffers for user-logged data from reward functions, flushed after gathering self._pending_extra_logs = defaultdict(list) self._pending_metrics = defaultdict(list) # Ensure each process receives a unique seed to prevent duplicate completions when generating with # transformers if num_generations exceeds per_device_train_batch_size. We could skip it if we use vLLM, but # it's safer to set it in all cases. set_seed(args.seed, device_specific=True) if self.use_vllm: # Initialize vLLM generation backend self.vllm_generation = VLLMGeneration( model=self.model, accelerator=self.accelerator, is_fsdp_enabled=self.is_fsdp_enabled, processing_class=self.processing_class, # vLLM configuration mode=args.vllm_mode, structured_outputs_regex=args.vllm_structured_outputs_regex, # Server mode configuration server_base_url=args.vllm_server_base_url, server_host=args.vllm_server_host, server_port=args.vllm_server_port, group_port=args.vllm_group_port, server_timeout=args.vllm_server_timeout, # Colocate mode configuration tensor_parallel_size=args.vllm_tensor_parallel_size, gpu_memory_utilization=args.vllm_gpu_memory_utilization, max_model_length=args.vllm_max_model_length, max_num_seqs=args.per_device_train_batch_size * args.vllm_tensor_parallel_size * args.steps_per_generation, enable_sleep_mode=args.vllm_enable_sleep_mode, model_impl=args.vllm_model_impl, # Generation configuration repetition_penalty=self.repetition_penalty, temperature=self.temperature, top_p=self.top_p, top_k=self.top_k, min_p=self.min_p, max_completion_length=self.max_completion_length, logprobs=None, # we don't need logprobs from vLLM in RLOO generation_kwargs=args.generation_kwargs, ) self._last_loaded_step = -1 # tag to avoid useless loading during grad accumulation else: generation_kwargs = { "max_new_tokens": self.max_completion_length, "do_sample": True, "pad_token_id": tokenizer.pad_token_id, "bos_token_id": tokenizer.bos_token_id, "eos_token_id": tokenizer.eos_token_id, "temperature": self.temperature, "top_p": self.top_p, "top_k": self.top_k, "min_p": self.min_p, "repetition_penalty": self.repetition_penalty, "cache_implementation": args.cache_implementation, } if args.generation_kwargs is not None: generation_kwargs.update(args.generation_kwargs) self.generation_config = GenerationConfig(**generation_kwargs) # Keep training-specific generation kwargs to overwrite model's original generation config self.generation_kwargs = generation_kwargs # Gradient accumulation requires scaled loss. Normally, loss scaling in the parent class depends on whether the # model accepts loss-related kwargs. Since we compute our own loss, this check is irrelevant. We set # self.model_accepts_loss_kwargs to False to enable scaling. self.model_accepts_loss_kwargs = False # Add tags to the model self.model.add_model_tags(self._tag_names) if self.ref_model is not None: if self.is_deepspeed_enabled: self.ref_model = prepare_deepspeed(self.ref_model, self.accelerator) elif self.is_fsdp_enabled: self.ref_model = prepare_fsdp(self.ref_model, self.accelerator) else: self.ref_model = self.accelerator.prepare_model(self.ref_model, evaluation_mode=True) if args.sync_ref_model: if self.beta == 0.0: raise ValueError( "You passed `sync_ref_model=True` while `beta=0.0`, which means the reference model is not used " "during training. Consequently, RLOOTrainer does not create a `ref_model` instance, and there is " "nothing to synchronize. Please set `sync_ref_model=False`, or set `beta` to a non-zero value." ) if is_peft_model(model): raise NotImplementedError( "You passed `sync_ref_model=True` while using a PEFT model, which is currently not supported. " "With PEFT, RLOOTrainer does not keep a separate reference model in memory; instead, it recovers " "reference behavior by temporarily disabling the adapter. As a result, there is no standalone " "`ref_model` instance to synchronize. Use `sync_ref_model=False`, or opt for full fine-tuning if " "you need a synced reference model. If you need `sync_ref_model` to work with PEFT, please open a " "feature request at https://github.com/huggingface/trl/issues." ) self.add_callback(SyncRefModelCallback(ref_model=self.ref_model, accelerator=self.accelerator)) for i, reward_func in enumerate(self.reward_funcs): if isinstance(reward_func, PreTrainedModel): if self.is_deepspeed_enabled: self.reward_funcs[i] = prepare_deepspeed(reward_func, self.accelerator) else: # set device placement to True to make `prepare_model` move `reward_func` to device when using fsdp self.reward_funcs[i] = self.accelerator.prepare_model( reward_func, evaluation_mode=True, device_placement=True ) def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). In RLOOTrainer, we preprocess data, so using the model's signature columns doesn't # work. Instead, we set them to the columns expected by the `training_step` method, hence the override. if self._signature_columns is None: self._signature_columns = ["prompt", "image", "images"] # This method overrides `Trainer.get_train_dataloader` to support our custom batching strategy. # Instead of returning a standard per-step batch (i.e., `per_device_batch_size), our dataloader loads an # *generation* batch (i.e., `per_device_batch_size × steps_per_generation`). This allows us to generate completions # once every steps_per_generation step—rather than once per accumulation step—which is significantly more # efficient. The only change from the original implementation is multiplying the batch size by # `steps_per_generation`. Thus, `_prepare_inputs` is called with this *generation* batch, and it handles the # splitting internally. # Maintenance note: This method is a copy-paste of the original `Trainer.get_train_dataloader` with only one line # modification. def get_train_dataloader(self): return self._get_dataloader( dataset=self.train_dataset, description="Training", batch_size=self._train_batch_size * self.args.steps_per_generation, # < this is the change sampler_fn=self._get_train_sampler, is_training=True, ) def _get_train_sampler(self, dataset: Dataset | None = None) -> Sampler: # Returns a sampler that # 1. ensures each prompt is repeated across multiple processes. This guarantees that identical prompts are # distributed to different GPUs, allowing rewards to be computed and normalized correctly within each prompt # group. Using the same seed across processes ensures consistent prompt assignment, preventing discrepancies # in group formation. # 2. repeats the batch multiple times to allow reusing generations across multiple updates. Refer to # _prepare_inputs to see how the generations are stored and reused. # In the following figure, the values are the prompt indices. The first row shows the first sampled batch, the # second row shows the second sampled batch, and so on. # # | GPU 0 | GPU 1 | # # global_step step <-───> num_generations=2 # <-───────> per_device_train_batch_size=3 # grad_accum ▲ ▲ 0 0 0 0 1 1 2 2 <- Generate for the first `steps_per_generation` (prompts 0 to 11); store the completions; use the first slice to compute the loss # =2 ▼ | 0 1 3 3 4 4 5 5 <- Take the stored generations and use the second slice to compute the loss # | # | 1 2 6 6 7 7 8 8 <- Take the stored generations and use the third slice to compute the loss # steps_per_gen=4 ▼ 1 3 9 9 10 10 11 11 <- Take the stored generations and use the fourth slice to compute the loss # # 2 4 12 12 13 13 14 14 <- Generate for the second `steps_per_generation` (prompts 12 to 23); store the completions; use the first slice to compute the loss # 2 5 15 15 16 16 17 17 <- Take the stored generations and use the second slice to compute the loss # ... if dataset is None: dataset = self.train_dataset return RepeatSampler( data_source=dataset, mini_repeat_count=self.num_generations, batch_size=self.args.generation_batch_size // self.num_generations, repeat_count=self.num_iterations * self.args.steps_per_generation, shuffle=self.shuffle_dataset, seed=self.args.seed, ) def _get_eval_sampler(self, eval_dataset) -> Sampler: # See _get_train_sampler for an explanation of the sampler. return RepeatSampler( data_source=eval_dataset, mini_repeat_count=self.num_generations_eval, seed=self.args.seed, ) @profiling_decorator def _get_per_token_logps_and_entropies( self, model, input_ids, attention_mask, logits_to_keep, batch_size=None, compute_entropy=False, pixel_values=None, image_grid_thw=None, num_images=None, pixel_attention_mask=None, image_sizes=None, token_type_ids=None, mm_token_type_ids=None, ) -> dict[str, torch.Tensor | None]: """Compute log-probs and (optionally) entropies for each token.""" batch_size = batch_size or input_ids.size(0) # Chunk inputs into smaller batches to reduce memory peak all_logps = [] all_entropies = [] for start in range(0, input_ids.size(0), batch_size): input_ids_batch = input_ids[start : start + batch_size] attention_mask_batch = attention_mask[start : start + batch_size] # Build model inputs - check if the model supports logits_to_keep (some models and VLMs don't) model_inputs = {"input_ids": input_ids_batch, "attention_mask": attention_mask_batch} if image_grid_thw is not None and pixel_values is not None: rows_per_image = image_grid_thw.prod(dim=-1) rows_per_sample = torch.split(rows_per_image, num_images) rows_per_sample = torch.stack([s.sum() for s in rows_per_sample]) cum_rows = torch.cat([torch.tensor([0], device=rows_per_sample.device), rows_per_sample.cumsum(0)]) row_start, row_end = cum_rows[start].item(), cum_rows[start + batch_size].item() model_inputs["pixel_values"] = pixel_values[row_start:row_end] cum_imgs = torch.tensor([0] + num_images).cumsum(0) img_start, img_end = cum_imgs[start], cum_imgs[start + batch_size] model_inputs["image_grid_thw"] = image_grid_thw[img_start:img_end] elif pixel_values is not None: model_inputs["pixel_values"] = pixel_values[start : start + batch_size] if pixel_attention_mask is not None: model_inputs["pixel_attention_mask"] = pixel_attention_mask[start : start + batch_size] if image_sizes is not None: model_inputs["image_sizes"] = image_sizes[start : start + batch_size] if token_type_ids is not None: model_inputs["token_type_ids"] = token_type_ids[start : start + batch_size] if mm_token_type_ids is not None: model_inputs["mm_token_type_ids"] = mm_token_type_ids[start : start + batch_size] # Only add logits_to_keep if the model supports it if "logits_to_keep" in self.model_kwarg_keys: # We add 1 to `logits_to_keep` because the last logits of the sequence is later excluded model_inputs["logits_to_keep"] = logits_to_keep + 1 model_inputs["use_cache"] = False # only used in generation; set False to suppress warnings logits = model(**model_inputs).logits # Exclude the last value: it corresponds to the next token pred logits = logits[:, :-1, :] # (B, L-1, H) # Only keep the last logits_to_keep. For model that support logits_to_keep, this is a no-op. logits = logits[:, -logits_to_keep:, :] # (B, logits_to_keep, H) # Divide logits by sampling temperature. # See https://huggingface.co/blog/the_n_implementation_details_of_rlhf_with_ppo#policy-training-implementation-details logits = logits / self.temperature completion_ids = input_ids_batch[:, -logits_to_keep:] logps = selective_log_softmax(logits, completion_ids) # compute logprobs all_logps.append(logps) if compute_entropy: with torch.no_grad(): entropies = entropy_from_logits(logits) all_entropies.append(entropies) logps = torch.cat(all_logps, dim=0) entropies = torch.cat(all_entropies, dim=0) if compute_entropy else None return logps, entropies def training_step(self, model, inputs, num_items_in_batch): time_before = time.perf_counter() output = super().training_step(model, inputs, num_items_in_batch) self._step += 1 time_after = time.perf_counter() self._current_train_step_time += time_after - time_before if self._step % self.current_gradient_accumulation_steps == 0: self._metrics["train"]["step_time"].append(self._current_train_step_time) self._current_train_step_time = 0.0 return output @profiling_decorator def _prepare_inputs(self, generation_batch: dict[str, torch.Tensor | Any]) -> dict[str, torch.Tensor | Any]: # Prepares inputs for model training/evaluation by managing completion generation and batch handling. # During training: # - Receives the local generation batch (Per-GPU batch size × steps per generation) # from the modified training dataloader instead of the standard local batch # - Generates completions once for the entire generation batch and splits it into batches of size # `per_device_train_batch_size` # - Buffers these completions and returns the appropriate slice for the current accumulation step # - Optimizes by regenerating completions only periodically (every steps_per_generation * num_iterations) # During evaluation: # - The input is treated as a standard local batch (no accumulation, no multiple iterations) # - Completions are generated for each batch without buffering or reuse # Returns a single local batch in both cases. mode = "train" if self.model.training else "eval" if mode == "train": generate_every = self.args.steps_per_generation * self.num_iterations if self._step % generate_every == 0 or self._buffered_inputs is None: # self._buffered_inputs=None can occur when resuming from a checkpoint generation_batch = self._generate_and_score_completions(generation_batch) generation_batch = split_pixel_values_by_grid(generation_batch) generation_batch = shuffle_sequence_dict(generation_batch) generation_batches = split_tensor_dict(generation_batch, self.args.steps_per_generation) self._buffered_inputs = [unsplit_pixel_values_by_grid(batch) for batch in generation_batches] inputs = self._buffered_inputs[self._step % self.args.steps_per_generation] else: # In evaluation, there is neither batch grouping for generation, nor multiple iterations, hence # local generation batch == local eval batch inputs = self._generate_and_score_completions(generation_batch) return inputs def _log_completion_extra(self, column: str, values: list): """ Log extra columns to the completions table. Called from reward functions via the `log_extra` kwarg. Args: column (`str`): Name of the column to add. values (`list`): Values for the column, one per sample in the batch. """ self._pending_extra_logs[column].extend(values) def _log_metric(self, name: str, value: float): """ Log a scalar metric from a reward function. Called via the `log_metric` kwarg. Values are averaged over each logging step and reported alongside built-in metrics like `kl` and `entropy`. Args: name (`str`): Name of the metric. value (`float`): Scalar value for this batch. """ self._pending_metrics[name].append(value) @profiling_decorator def _calculate_rewards(self, inputs, prompts, completions, completion_ids_list): device = self.accelerator.device rewards_per_func = torch.zeros(len(prompts), len(self.reward_funcs), device=device) # Repeat all input columns (but "prompt", "completion", and "completion_ids") to match the num of generations keys = [key for key in inputs[0] if key not in ["prompt", "completion", "completion_ids"]] reward_kwargs = {key: [example[key] for example in inputs] for key in keys} # This allows for dynamic reward shaping based on training progress. reward_kwargs["trainer_state"] = self.state # Allow reward functions to log extra columns to the completions table. reward_kwargs["log_extra"] = self._log_completion_extra # Allow reward functions to log additional scalar metrics. reward_kwargs["log_metric"] = self._log_metric async_funcs_info = [] # async custom functions for asyncio.gather for i, (reward_func, reward_processing_class, reward_func_name) in enumerate( zip(self.reward_funcs, self.reward_processing_classes, self.reward_func_names, strict=True) ): if isinstance(reward_func, nn.Module): # Module (no PretrainedModel) for compat with compiled models with profiling_context(self, reward_func_name): if is_conversational(inputs[0]): messages = [{"messages": p + c} for p, c in zip(prompts, completions, strict=True)] texts = [ apply_chat_template(x, reward_processing_class, **self.chat_template_kwargs)["text"] for x in messages ] else: texts = [p + c for p, c in zip(prompts, completions, strict=True)] reward_inputs = reward_processing_class( text=texts, return_tensors="pt", padding=True, padding_side="right", add_special_tokens=False ) reward_inputs = super()._prepare_inputs(reward_inputs) with torch.inference_mode(): rewards_per_func[:, i] = reward_func(**reward_inputs).logits[:, 0] # Shape (B*G,) elif inspect.iscoroutinefunction(reward_func): # Separate async reward funcs to run them in parallel later async_funcs_info.append((i, reward_func, reward_func_name)) else: # Run synchronous reward function with profiling_context(self, reward_func_name): output_reward_func = reward_func( prompts=prompts, completions=completions, completion_ids=completion_ids_list, **reward_kwargs ) # Convert None values to NaN output_reward_func = [reward if reward is not None else torch.nan for reward in output_reward_func] rewards_per_func[:, i] = torch.tensor(output_reward_func, dtype=torch.float32, device=device) # Execute async custom functions in parallel using asyncio.gather if async_funcs_info: async def _invoke_async_reward(index, func, func_name): with profiling_context(self, func_name): output = await func( prompts=prompts, completions=completions, completion_ids=completion_ids_list, **reward_kwargs ) output = [r if r is not None else torch.nan for r in output] return index, output async def _run_async_funcs(): coros = [_invoke_async_reward(i, func, func_name) for (i, func, func_name) in async_funcs_info] return await asyncio.gather(*coros) async_results = asyncio.run_coroutine_threadsafe(_run_async_funcs(), self.async_reward_loop).result() for idx, output_reward_func in async_results: rewards_per_func[:, idx] = torch.tensor(output_reward_func, dtype=torch.float32, device=device) # If all reward functions return None for a given row, issue a detailed warning if torch.isnan(rewards_per_func).all(dim=1).any(): nan_row_idx = torch.isnan(rewards_per_func).all(dim=1).nonzero(as_tuple=True)[0][0] row_reward_kwargs = { key: value[nan_row_idx] for key, value in reward_kwargs.items() if key not in ("trainer_state", "log_extra", "log_metric") } row_reward_kwargs["prompt"] = prompts[nan_row_idx] row_reward_kwargs["completion"] = completions[nan_row_idx] logger.warning( f"All reward functions returned None for the following kwargs:\n{row_reward_kwargs}\n" "Please ensure that at least one reward function returns a valid reward." ) # Gather the reward per function: this part is crucial, because the rewards are normalized per group and the # completions may be distributed across processes rewards_per_func = gather(rewards_per_func) return rewards_per_func def _tokenize_prompts(self, prompts: list): """Tokenize prompts and extract images/multimodal fields for generation.""" if is_conversational({"prompt": prompts[0]}): # Extract images from messages for VLM support images = [] has_images = False for prompt in prompts: prompt_images = [] for message in prompt: if isinstance(message["content"], list): for part in message["content"]: if part["type"] == "image": prompt_images.append(part["image"]) has_images = True images.append(prompt_images if prompt_images else None) images = images if has_images else None # We pass padding=True to work around a bug introduced in transformers 5.2.0 in some processors # (e.g. Qwen2.5-VL) that crash on batched unpadded input. We then unpad input_ids using attention_mask. # See: https://github.com/huggingface/transformers/issues/44514 tokenized = self.processing_class.apply_chat_template( conversation=prompts, add_generation_prompt=True, tokenize=True, return_dict=True, padding=True, **self.chat_template_kwargs, ) # Unpad input_ids: remove padding tokens using attention_mask to get per-sequence lists prompt_ids = [ [tok for tok, m in zip(ids, mask, strict=True) if m] for ids, mask in zip(tokenized["input_ids"], tokenized["attention_mask"], strict=True) ] # For VLMs, the processor returns extra multimodal fields (pixel_values, image_grid_thw, etc.) multimodal_fields = {k: v for k, v in tokenized.items() if k not in ("input_ids", "attention_mask")} else: prompt_ids = self.processing_class(text=prompts)["input_ids"] images = None multimodal_fields = {} return prompt_ids, images, multimodal_fields def _generate_single_turn(self, prompt_ids, images, multimodal_fields): device = self.accelerator.device mode = "train" if self.model.training else "eval" # Generate completions using either vLLM or regular generation if self.use_vllm: # Sync weights if training step changed if self.state.global_step != self._last_loaded_step: with profiling_context(self, "sync_weights"): self.vllm_generation.sync_weights() self._last_loaded_step = self.state.global_step # Generate using vLLM (note: RLOO doesn't use logprobs from generation, so we ignore them) num_generations = self.num_generations if mode == "train" else self.num_generations_eval _, completion_ids, _, _ = self.vllm_generation.generate( prompts=prompt_ids, images=images, num_generations=num_generations, profiler=profiling_context(self, "vLLM.generate"), ) elif self.use_transformers_paged: with ( profiling_context(self, "transformers.generate_batch"), unwrap_model_for_generation( self.model_wrapped, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): # Cast to the appropriate dtype based on training configuration if self.args.bf16: unwrapped_model.to(torch.bfloat16) elif self.args.fp16: unwrapped_model.to(torch.float16) with torch.inference_mode(): # Continuous batching API expects 'inputs' arg only all_outputs = unwrapped_model.generate_batch( prompt_ids, generation_config=self.generation_config, progress_bar=False ) unwrapped_model.train() # restore training mode, as generate_batch forces eval mode completion_ids = [output.generated_tokens for output in all_outputs.values()] else: # Regular generation path: left-pad token IDs into tensors prompt_tensors = [torch.tensor(ids) for ids in prompt_ids] padded_ids = pad(prompt_tensors, padding_value=self.pad_token_id, padding_side="left") attention_mask = pad([torch.ones_like(t) for t in prompt_tensors], padding_value=0, padding_side="left") generate_inputs = {"input_ids": padded_ids, "attention_mask": attention_mask} # For VLMs, include multimodal fields as tensors (pixel_values, image_grid_thw, etc.) for k, v in multimodal_fields.items(): if isinstance(v, torch.Tensor): generate_inputs[k] = v elif isinstance(v, list) and v and isinstance(v[0], list): # Per-token field (e.g., token_type_ids): left-pad like input_ids generate_inputs[k] = pad([torch.tensor(x) for x in v], padding_value=0, padding_side="left") else: generate_inputs[k] = torch.tensor(np.array(v)) generate_inputs = super()._prepare_inputs(generate_inputs) with ( profiling_context(self, "transformers.generate"), unwrap_model_for_generation( self.model_wrapped, self.accelerator, gather_deepspeed3_params=self.args.ds3_gather_for_generation, generation_kwargs=self.generation_kwargs, # Override model.generation_config with generation_kwargs to fix transformers#42762 ) as unwrapped_model, torch.no_grad(), FSDP.summon_full_params(self.model_wrapped, recurse=False) if self.is_fsdp_enabled else nullcontext(), ): prompt_completion_ids = unwrapped_model.generate( **generate_inputs, generation_config=self.generation_config, disable_compile=True ) # Compute prompt length and extract completion ids prompt_length = generate_inputs["input_ids"].size(1) completion_ids = prompt_completion_ids[:, prompt_length:] # Mask everything after the first EOS token is_eos = completion_ids == self.eos_token_id eos_idx = torch.full((is_eos.size(0),), is_eos.size(1), dtype=torch.long, device=device) eos_idx[is_eos.any(dim=1)] = is_eos.int().argmax(dim=1)[is_eos.any(dim=1)] sequence_indices = torch.arange(is_eos.size(1), device=device).expand(is_eos.size(0), -1) completion_mask = (sequence_indices <= eos_idx.unsqueeze(1)).int() completion_ids = [ c[m].tolist() for c, m in zip(completion_ids.cpu(), completion_mask.bool().cpu(), strict=True) ] return completion_ids def _generate(self, prompts: list): device = self.accelerator.device mode = "train" if self.model.training else "eval" # Copy the prompts to avoid modifying the original list prompts = copy.deepcopy(prompts) prompt_ids, images, multimodal_fields = self._tokenize_prompts(prompts) completion_ids = self._generate_single_turn(prompt_ids, images, multimodal_fields) # Decode completions. It's important to use `parse_response` when possible, because it handles tool calls. if is_conversational({"prompt": prompts[0]}): contents = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) completions = [[{"role": "assistant", "content": content}] for content in contents] else: completions = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Get completion length per sequence, used for logging prompt_lengths = torch.tensor([len(ids) for ids in prompt_ids], device=device) completion_lengths = torch.tensor([len(ids) for ids in completion_ids], device=device) agg_prompt_lengths = self.accelerator.gather(prompt_lengths) agg_completion_lengths = self.accelerator.gather(completion_lengths) total_prompt_tokens = agg_prompt_lengths.sum() total_completion_tokens = agg_completion_lengths.sum() # = num_items_in_batch, required for the DAPO loss # Log the metrics if mode == "train": self.state.num_input_tokens_seen += (total_prompt_tokens + total_completion_tokens).item() self._metrics[mode]["num_tokens"] = [self.state.num_input_tokens_seen] # Log completion lengths, mean, min, max self._metrics[mode]["completions/mean_length"].append(agg_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_length"].append(agg_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_length"].append(agg_completion_lengths.float().max().item()) # Identify sequences that terminated with EOS and log their lengths eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids], device=device) agg_is_truncated = self.accelerator.gather(is_truncated) self._metrics[mode]["completions/clipped_ratio"].append(agg_is_truncated.float().mean().item()) term_completion_lengths = agg_completion_lengths[~agg_is_truncated] if len(term_completion_lengths) == 0: # edge case where no terminated sequences are found term_completion_lengths = torch.zeros(1, device=device) self._metrics[mode]["completions/mean_terminated_length"].append(term_completion_lengths.float().mean().item()) self._metrics[mode]["completions/min_terminated_length"].append(term_completion_lengths.float().min().item()) self._metrics[mode]["completions/max_terminated_length"].append(term_completion_lengths.float().max().item()) return prompt_ids, completion_ids, completions def _generate_and_score_completions( self, inputs: list[dict[str, torch.Tensor | Any]] ) -> dict[str, torch.Tensor | Any]: device = self.accelerator.device mode = "train" if self.model.training else "eval" prompts = [x["prompt"] for x in inputs] if "images" in inputs[0]: images = [example.get("images") for example in inputs] elif "image" in inputs[0]: images = [[example.get("image")] if example.get("image") is not None else None for example in inputs] else: images = None # Transformers requires at least one image in the batch, otherwise it throws an error if images is not None and all(img_list == [] for img_list in images): images = None # If the prompts are conversational and the inputs contain images, we need to convert the prompts from # [{"role": "user", "content": "What color is the sky?"}] to # [{"role": "user", "content": [{"type": "image", "image": }, {"type": "text", "text": "What color is the sky?"}]}] if images is not None: if not is_conversational(inputs[0]): raise ValueError( "Multimodal training requires conversational prompts. It looks like the dataset contains " "non-conversational inputs, likely because a chat template was applied before passing the dataset " "to the trainer. Please provide the raw conversational prompts and let the trainer apply the chat " "template internally." ) prompts = [ prepare_multimodal_messages(prompt, image_list) for prompt, image_list in zip(prompts, images, strict=True) ] prompt_ids_list, completion_ids_list, completions = self._generate(prompts) # Convert lists of token IDs to padded tensors prompt_ids = [torch.tensor(ids) for ids in prompt_ids_list] prompt_mask = [torch.ones_like(ids, dtype=torch.long) for ids in prompt_ids] prompt_ids = pad( prompt_ids, padding_value=self.pad_token_id, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) prompt_mask = pad( prompt_mask, padding_value=0, padding_side="left", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_ids = [torch.tensor(ids) for ids in completion_ids_list] completion_mask = [torch.ones_like(ids, dtype=torch.long) for ids in completion_ids] completion_ids = pad( completion_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ).to(device=device) # If mask_truncated_completions is enabled, zero out truncated completions in completion_mask if self.mask_truncated_completions: eos_and_pad = [self.eos_token_id, self.pad_token_id] is_truncated = torch.tensor([ids[-1] not in eos_and_pad for ids in completion_ids_list], device=device) completion_mask = completion_mask * (~is_truncated).unsqueeze(1).int() # Concatenate prompt_mask with completion_mask for logit computation prompt_completion_ids = torch.cat([prompt_ids, completion_ids], dim=1) # (B, P+C) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) # (B, P+C) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size num_images = [len(img_list) for img_list in images] if images is not None else None # Get forward_kwargs for models with multimodal inputs if images is not None: prompts_text = [ apply_chat_template({"prompt": prompt}, self.processing_class, **self.chat_template_kwargs)["prompt"] for prompt in prompts ] prompt_inputs = self.processing_class(images=images, text=prompts_text, padding=True, return_tensors="pt") prompt_inputs = super()._prepare_inputs(prompt_inputs) forward_kwargs = {k: v for k, v in prompt_inputs.items() if k not in ["input_ids", "attention_mask"]} else: forward_kwargs = {} # If token_type_ids are used, extend them with zeros for the completion part if "token_type_ids" in forward_kwargs: token_type_ids = forward_kwargs["token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and token_type_ids must have equal len padding_size = prompt_ids.size(1) - token_type_ids.size(1) if padding_size > 0: token_type_ids = torch.cat( [token_type_ids.new_zeros((token_type_ids.size(0), padding_size)), token_type_ids], dim=1 ) forward_kwargs["token_type_ids"] = torch.cat( [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # If mm_token_type_ids are used, extend them with zeros for the completion part if "mm_token_type_ids" in forward_kwargs: mm_token_type_ids = forward_kwargs["mm_token_type_ids"] if self.pad_to_multiple_of is not None: # Needed only with pad_to_multiple_of: otherwise prompt_ids and mm_token_type_ids must have equal len padding_size = prompt_ids.size(1) - mm_token_type_ids.size(1) if padding_size > 0: mm_token_type_ids = torch.cat( [mm_token_type_ids.new_zeros((mm_token_type_ids.size(0), padding_size)), mm_token_type_ids], dim=1, ) forward_kwargs["mm_token_type_ids"] = torch.cat( [mm_token_type_ids, mm_token_type_ids.new_zeros(completion_ids.shape)], dim=1 ) # When gradient checkpointing is enabled with use_reentrant=True (non default), calling the model inside a # torch.no_grad() block triggers a harmless PyTorch warning ("None of the inputs have requires_grad=True"). # Temporarily disable checkpointing to avoid this warning during inference. with torch.no_grad(), disable_gradient_checkpointing(self.model, self.args.gradient_checkpointing_kwargs): # Compute the per-token log probabilities for the current model old_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) old_logps = (old_per_token_logps * completion_mask).sum(1) # mask out padding and tokens after EOS # Compute the per-token log probabilities for the reference model if self.beta != 0.0: if self.ref_model is not None: ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.ref_model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: # When training a PEFT adapter, how we obtain the reference depends on the setup: # - New adapter: disabling adapters yields the base model. # - Re-training an existing adapter: an initial copy is loaded under the name "ref". model = self.accelerator.unwrap_model(self.model) with use_adapter(model, adapter_name="ref" if "ref" in model.peft_config else None): ref_per_token_logps, _ = self._get_per_token_logps_and_entropies( self.model, prompt_completion_ids, attention_mask, logits_to_keep, batch_size=batch_size, num_images=num_images, **forward_kwargs, # may contain pixel_values, image_grid_thw, pixel_attention_mask and image_sizes ) else: ref_per_token_logps = None # Decode prompts_text = self.processing_class.batch_decode(prompt_ids, skip_special_tokens=True) completions_text = self.processing_class.batch_decode(completion_ids, skip_special_tokens=True) # Calculate rewards for each reward function. rewards_per_func aggregates rewards across all processes. This is # important because rewards will be normalized per group, and completions are distributed. We will later slice # rewards_per_func to extract each process's subset. rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list) num_generations = self.num_generations if mode == "train" else self.num_generations_eval # Apply weights to each reward function's output and sum rewards = (rewards_per_func * self.reward_weights.to(device).unsqueeze(0)).nansum(dim=1) # Apply reward clipping if specified if self.reward_clip_range: rewards = rewards.clamp(min=self.reward_clip_range[0], max=self.reward_clip_range[1]) # Include the KL penalty in the reward if self.beta != 0.0: per_token_kl = old_per_token_logps - ref_per_token_logps # Apply sequence-level KL penalty to rewards (sum KL across tokens first, then apply to each sequence) kl = (per_token_kl * completion_mask).sum(-1) kl = gather(kl) # rewards are gathered, so kl must be too rewards = rewards - self.beta * kl grouped_rewards = rewards.view(-1, num_generations) mean_grouped_rewards = grouped_rewards.mean(dim=1) if num_generations > 1: std_rewards = grouped_rewards.std(dim=1) else: # doesn't occur during training, but could occur in eval when num_generations_eval=1 std_rewards = torch.zeros_like(mean_grouped_rewards) # RLOO advantages computation grouped_sum = grouped_rewards.sum(dim=1, keepdim=True) # (num_prompts, 1) if num_generations > 1: baselines = (grouped_sum - grouped_rewards) / (num_generations - 1) # (num_prompts, num_generations) baselines = baselines.view(-1) # Flatten back to match rewards shape advantages = rewards - baselines else: # this case doesn't occur during training, but could in eval when num_generations_eval=1 advantages = torch.zeros_like(rewards) # Normalize advantages if self.normalize_advantages: advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-4) is_std_zero = torch.isclose(std_rewards, torch.zeros_like(std_rewards)) # for logging # Slice to keep only the local part of the data process_slice = slice( self.accelerator.process_index * len(prompts), (self.accelerator.process_index + 1) * len(prompts), ) all_process_advantages = advantages.clone() # keep the aggregated advantages for logging advantages = advantages[process_slice] # Calculate and log the mean KL divergence between current and reference model if self.beta != 0.0: mean_kl = (per_token_kl * completion_mask).sum() / completion_mask.sum().clamp(min=1.0) self._metrics[mode]["kl"].append(self.accelerator.gather(mean_kl).nanmean().item()) # Calculate mean reward per function, but only for samples where the function was applied (non-NaN values) for i, reward_func_name in enumerate(self.reward_func_names): mean_rewards = torch.nanmean(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/mean"].append(mean_rewards) std_func_rewards = nanstd(rewards_per_func[:, i]).item() self._metrics[mode][f"rewards/{reward_func_name}/std"].append(std_func_rewards) rewards = rewards_per_func.nansum(dim=1) self._metrics[mode]["reward"].append(rewards.mean().item()) self._metrics[mode]["reward_std"].append(rewards.std().item()) self._metrics[mode]["frac_reward_zero_std"].append(is_std_zero.float().mean().item()) # Log prompt and completion texts self._logs["prompt"].extend(gather_object(prompts_text)) self._logs["completion"].extend(gather_object(completions_text)) for i, name in enumerate(self.reward_func_names): self._logs["rewards"][name].extend(rewards_per_func[:, i].tolist()) self._logs["advantages"].extend(all_process_advantages.tolist()) # Flush user-logged extra columns (from log_extra), gathering across processes. # Keys must be sorted so that all ranks call gather_object in the same order, otherwise values # get mis-attributed across columns (dict insertion order may differ between processes). for column in sorted(self._pending_extra_logs): self._logs["extra"][column].extend(gather_object(self._pending_extra_logs[column])) self._pending_extra_logs.clear() # Flush user-logged metrics (from log_metric), averaging across processes. # Keys must be sorted so that all ranks call accelerator.gather in the same order, otherwise values # get mis-attributed across metrics (dict insertion order may differ between processes). for name in sorted(self._pending_metrics): values = self._pending_metrics[name] local_mean = sum(values) / len(values) global_mean = self.accelerator.gather(torch.tensor(local_mean, device=device)).mean().item() self._metrics[mode][name].append(global_mean) self._pending_metrics.clear() if images is not None: self._logs["images"].extend(gather_object(images)) output = { "prompt_ids": prompt_ids, "prompt_mask": prompt_mask, "completion_ids": completion_ids, "completion_mask": completion_mask, "old_logps": old_logps, "advantages": advantages, } if "pixel_values" in forward_kwargs: output["pixel_values"] = forward_kwargs["pixel_values"] if "image_grid_thw" in forward_kwargs: output["image_grid_thw"] = forward_kwargs["image_grid_thw"] if "pixel_attention_mask" in forward_kwargs: output["pixel_attention_mask"] = forward_kwargs["pixel_attention_mask"] if "image_sizes" in forward_kwargs: output["image_sizes"] = forward_kwargs["image_sizes"] if "token_type_ids" in forward_kwargs: output["token_type_ids"] = forward_kwargs["token_type_ids"] if "mm_token_type_ids" in forward_kwargs: output["mm_token_type_ids"] = forward_kwargs["mm_token_type_ids"] if images is not None: output["num_images"] = num_images return output @profiling_decorator def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): if return_outputs: raise ValueError("The RLOOTrainer does not support returning outputs") return self._compute_loss(model, inputs) def _compute_loss(self, model, inputs): # Compute the per-token log probabilities for the model prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"] completion_ids, completion_mask = inputs["completion_ids"], inputs["completion_mask"] input_ids = torch.cat([prompt_ids, completion_ids], dim=1) attention_mask = torch.cat([prompt_mask, completion_mask], dim=1) logits_to_keep = completion_ids.size(1) # we only need to compute the logits for the completion tokens # Compute the per_token_logps and the entropy at each position in the completion per_token_logps, entropies = self._get_per_token_logps_and_entropies( model, input_ids, attention_mask, logits_to_keep, compute_entropy=True, pixel_values=inputs.get("pixel_values"), image_grid_thw=inputs.get("image_grid_thw"), num_images=inputs.get("num_images"), pixel_attention_mask=inputs.get("pixel_attention_mask"), image_sizes=inputs.get("image_sizes"), token_type_ids=inputs.get("token_type_ids"), mm_token_type_ids=inputs.get("mm_token_type_ids"), ) logps = (per_token_logps * completion_mask).sum(1) # mask out padding and tokens after EOS old_logps = inputs["old_logps"] log_ratio = logps - old_logps # Compute the loss advantages = inputs["advantages"] coef_1 = torch.exp(log_ratio) coef_2 = torch.clamp(coef_1, 1 - self.epsilon_low, 1 + self.epsilon_high) per_sequence_loss1 = coef_1 * advantages per_sequence_loss2 = coef_2 * advantages per_sequence_loss = -torch.min(per_sequence_loss1, per_sequence_loss2) loss = per_sequence_loss.mean() # Log the metrics mode = "train" if self.model.training else "eval" # Entropy mean_entropy = (entropies * completion_mask).sum() / completion_mask.sum().clamp(min=1.0) self._metrics[mode]["entropy"].append(self.accelerator.gather(mean_entropy).nanmean().item()) # Compute the clipped probability ratios is_low_clipped = (coef_1 < 1 - self.epsilon_low) & (advantages < 0) is_high_clipped = (coef_1 > 1 + self.epsilon_high) & (advantages > 0) is_region_clipped = is_low_clipped | is_high_clipped gathered_low_clip = self.accelerator.gather(is_low_clipped.float().mean()) self._metrics[mode]["clip_ratio/low_mean"].append(gathered_low_clip.nanmean().item()) self._metrics[mode]["clip_ratio/low_min"].append(nanmin(gathered_low_clip).item()) gathered_high_clip = self.accelerator.gather(is_high_clipped.float().mean()) self._metrics[mode]["clip_ratio/high_mean"].append(gathered_high_clip.nanmean().item()) self._metrics[mode]["clip_ratio/high_max"].append(nanmax(gathered_high_clip).item()) gathered_clip_ratio = self.accelerator.gather(is_region_clipped.float().mean()) self._metrics[mode]["clip_ratio/region_mean"].append(gathered_clip_ratio.nanmean().item()) return loss # During eval, Trainer calls prediction_step. If no labels are present in the inputs, it only runs forward and # returns logits. We override prediction_step to force compute_loss, because this trainer doesn't involve labels. def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys: list[str] | None = None): inputs = self._prepare_inputs(inputs) with torch.no_grad(): with self.compute_loss_context_manager(): loss = self.compute_loss(model, inputs) loss = loss.mean().detach() return loss, None, None def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() if self.accelerator.is_main_process and self.log_completions: if is_rich_available(): print_prompt_completions_sample( self._logs["prompt"], self._logs["completion"], self._logs["rewards"], self._logs["advantages"], self.state.global_step, self.num_completions_to_print, ) logging_backends = [] if self.args.report_to and "wandb" in self.args.report_to and wandb.run is not None: logging_backends.append(wandb) if self.args.report_to and "trackio" in self.args.report_to: logging_backends.append(trackio) table = { "step": [str(self.state.global_step)] * len(self._logs["prompt"]), "prompt": self._logs["prompt"], "completion": self._logs["completion"], **self._logs["rewards"], **self._logs["extra"], "advantage": self._logs["advantages"], } df_base = pd.DataFrame(table) images_raw = self._logs["images"] or [] for logging_backend in logging_backends: if images_raw: images = [] for image_list in self._logs["images"]: images.append([logging_backend.Image(image) for image in image_list]) df = pd.concat( [df_base, pd.Series(images, name="image")], axis=1, copy=False, ) else: df = df_base if self.log_unique_prompts: df = df.drop_duplicates(subset=["prompt"]) logging_backend.log({"completions": logging_backend.Table(dataframe=df)}) # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/trainer/sft_config.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from dataclasses import dataclass, field from typing import Any from .base_config import _BaseConfig @dataclass class SFTConfig(_BaseConfig): # docstyle-ignore r""" Configuration class for the [`SFTTrainer`]. This class includes only the parameters that are specific to SFT training. For a full list of training arguments, please refer to the [`~transformers.TrainingArguments`] documentation. Note that default values in this class may differ from those in [`~transformers.TrainingArguments`]. Using [`~transformers.HfArgumentParser`] we can turn this class into [argparse](https://docs.python.org/3/library/argparse#module-argparse) arguments that can be specified on the command line. Parameters: > Parameters that control the model model_init_kwargs (`dict[str, Any]`, *optional*): Keyword arguments for [`~transformers.AutoModelForCausalLM.from_pretrained`], used when the `model` argument of the [`SFTTrainer`] is provided as a string. If you're training a MoE architecture and want to include the load balancing/auxiliary loss as a part of the final loss, remember to set `output_router_logits=True` in this dictionary. chat_template_path (`str`, *optional*): If specified, sets the model's chat template. This can either be the path to a tokenizer (local directory or Hugging Face Hub model) or a direct path to a Jinja template file. When using a Jinja file, you must ensure that any special tokens referenced in the template are added to the tokenizer and that the model's embedding layer is resized accordingly. > Parameters that control the data preprocessing dataset_text_field (`str`, *optional*, defaults to `"text"`): Name of the column that contains text data in the dataset. dataset_kwargs (`dict[str, Any]`, *optional*): Dictionary of optional keyword arguments for the dataset preparation. The only supported key is `skip_prepare_dataset`. When the model is a VLM, `skip_prepare_dataset` is automatically treated as `True` regardless of the provided value, since preprocessing is done on the fly. dataset_num_proc (`int`, *optional*): Number of processes to use for processing the dataset. eos_token (`str`, *optional*): Token used to indicate the end of a turn or sequence. If `None`, it defaults to `processing_class.eos_token`. pad_token (`str`, *optional*): Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that is also `None`, it falls back to `processing_class.eos_token`. max_length (`int` or `None`, *optional*, defaults to `1024`): Maximum length of the tokenized sequence. Sequences longer than `max_length` are truncated from the left or right depending on `truncation_mode`. If `None`, no truncation is applied. When packing is enabled, this value sets the sequence length. truncation_mode (`str`, *optional*, defaults to `"keep_start"`): Truncation mode to use when the sequence exceeds `max_length`. Possible values are `"keep_end"` and `"keep_start"`. shuffle_dataset (`bool`, *optional*, defaults to `False`): Whether to shuffle the dataset. packing (`bool`, *optional*, defaults to `False`): Whether to group multiple sequences into fixed-length blocks to improve computational efficiency and reduce padding. Uses `max_length` to define sequence length. packing_strategy (`str`, *optional*, defaults to `"bfd"`): Strategy for packing sequences. Can be `"bfd"` (best-fit decreasing, truncates overflow), `"bfd_split"` (best-fit decreasing, splits overflow sequences), or `"wrapped"` (aggressive, cuts mid-sequence). padding_free (`bool`, *optional*, defaults to `False`): Whether to perform forward passes without padding by flattening all sequences in the batch into a single continuous sequence. This reduces memory usage by eliminating padding overhead. Currently, this is only supported with the FlashAttention 2 or 3, which can efficiently handle the flattened batch structure. When packing is enabled with strategy `"bfd"`, padding-free is enabled, regardless of the value of this parameter. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. eval_packing (`bool`, *optional*): Whether to pack the eval dataset. If `None`, uses the same value as `packing`. > Parameters that control the training completion_only_loss (`bool`, *optional*): Whether to compute loss only on the completion part of the sequence. If set to `True`, loss is computed only on the completion, which is supported only for [prompt-completion](#prompt-completion) datasets. If `False`, loss is computed on the entire sequence. If `None` (default), the behavior depends on the dataset: loss is computed on the completion for [prompt-completion](#prompt-completion) datasets, and on the full sequence for [language modeling](#language-modeling) datasets. assistant_only_loss (`bool`, *optional*, defaults to `False`): Whether to compute loss only on the assistant part of the sequence. If set to `True`, loss is computed only on the assistant responses, which is supported only for [conversational](#conversational) datasets. If `False`, loss is computed on the entire sequence. loss_type (`str`, *optional*, defaults to `"nll"`): Type of loss to use. Possible values are `"nll"` (negative log-likelihood, default) and `"dft"` (Dynamic Fine-Tuning, as described in [this paper](https://huggingface.co/papers/2508.05629)). activation_offloading (`bool`, *optional*, defaults to `False`): Whether to offload the activations to the CPU. > [!NOTE] > These parameters have default values different from [`~transformers.TrainingArguments`]: > - `logging_steps`: Defaults to `10` instead of `500`. > - `gradient_checkpointing`: Defaults to `True` instead of `False`. > - `bf16`: Defaults to `True` if `fp16` is not set, instead of `False`. > - `learning_rate`: Defaults to `2e-5` instead of `5e-5`. """ _VALID_DICT_FIELDS = _BaseConfig._VALID_DICT_FIELDS + ["model_init_kwargs"] # Parameters whose default values are overridden from TrainingArguments learning_rate: float = field( default=2e-5, metadata={"help": "The initial learning rate for AdamW."}, ) # Parameters that control the model model_init_kwargs: dict[str, Any] | str | None = field( default=None, metadata={ "help": "Keyword arguments for `AutoModelForCausalLM.from_pretrained`, used when the `model` argument of " "the `SFTTrainer` is provided as a string. If you're training a MoE architecture and want to include the " "load balancing/auxiliary loss as a part of the final loss, remember to set `output_router_logits=True` " "in this dictionary." }, ) chat_template_path: str | None = field( default=None, metadata={ "help": "If specified, sets the model's chat template. This can either be the path to a tokenizer (local " "directory or Hugging Face Hub model) or a direct path to a Jinja template file. When using a Jinja file, " "you must ensure that any special tokens referenced in the template are added to the tokenizer and " "that the model's embedding layer is resized accordingly." }, ) # Parameters that control the data preprocessing dataset_text_field: str = field( default="text", metadata={"help": "Name of the column that contains text data in the dataset."}, ) dataset_kwargs: dict[str, Any] | None = field( default=None, metadata={ "help": "Dictionary of optional keyword arguments for the dataset preparation. The only supported key is " "`skip_prepare_dataset`. If the model is a VLM, `skip_prepare_dataset` value is ignored. When the model " "is a VLM, `skip_prepare_dataset` is automatically treated as `True` regardless of the provided value, " "since preprocessing is done on the fly." }, ) dataset_num_proc: int | None = field( default=None, metadata={"help": "Number of processes to use for processing the dataset."}, ) eos_token: str | None = field( default=None, metadata={ "help": "Token used to indicate the end of a turn or sequence. If `None`, it defaults to `processing_class.eos_token`." }, ) pad_token: str | None = field( default=None, metadata={ "help": "Token used for padding. If `None`, it defaults to `processing_class.pad_token`, or if that " "is also `None`, it falls back to `processing_class.eos_token`." }, ) max_length: int | None = field( default=1024, metadata={ "help": "Maximum length of the tokenized sequence. Sequences longer than `max_length` are truncated from " "the left or right depending on the `truncation_mode`. If `None`, no truncation is applied. When packing " "is enabled, this value sets the sequence length." }, ) truncation_mode: str = field( default="keep_start", metadata={ "help": "Truncation mode to use when the sequence exceeds `max_length`. Possible values are `'keep_end'` " "and `'keep_start'`.", "choices": ["keep_end", "keep_start"], }, ) shuffle_dataset: bool = field( default=False, metadata={"help": "Whether to shuffle the dataset."}, ) packing: bool = field( default=False, metadata={ "help": "Whether to group multiple sequences into fixed-length blocks to improve computational efficiency " "and reduce padding. Uses `max_length` to define sequence length." }, ) packing_strategy: str = field( default="bfd", metadata={ "help": "Strategy for packing sequences. Can be `'bfd'` (best-fit decreasing, truncates overflow), " "`'bfd_split'` (best-fit decreasing, splits overflow sequences), or `'wrapped'` (aggressive, cuts " "mid-sequence).", "choices": ["bfd", "bfd_split", "wrapped"], }, ) padding_free: bool = field( default=False, metadata={ "help": "Whether to perform forward passes without padding by flattening all sequences in the batch into " "a single continuous sequence. This reduces memory usage by eliminating padding overhead. Currently, this " "is only supported with the FlashAttention 2 or 3, which can efficiently handle the flattened batch " "structure. When packing is enabled with strategy `'bfd'`, padding-free is enabled, regardless of the " "value of this parameter." }, ) pad_to_multiple_of: int | None = field( default=None, metadata={"help": "If set, the sequences will be padded to a multiple of this value."}, ) eval_packing: bool | None = field( default=None, metadata={"help": "Whether to pack the eval dataset. If `None`, uses the same value as `packing`."}, ) # Parameters that control the training completion_only_loss: bool | None = field( default=None, metadata={ "help": ( "Whether to compute loss only on the completion part of the sequence. If set to `True`, loss is " "computed only on the completion, which is supported only for prompt-completion datasets. If `False`, " "loss is computed on the entire sequence. If `None` (default), the behavior depends on the dataset: " "loss is computed on the completion for prompt-completion datasets, and on the full sequence for " "language modeling datasets." ) }, ) assistant_only_loss: bool = field( default=False, metadata={ "help": ( "Whether to compute loss only on the assistant part of the sequence. If set to `True`, loss is " "computed only on the assistant responses, which is supported only for conversational datasets. If `False`, " "loss is computed on the entire sequence." ) }, ) loss_type: str = field( default="nll", metadata={ "help": ( 'Type of loss to use. Possible values are `"nll"` (negative log-likelihood, default) and `"dft"` ' "(Dynamic Fine-Tuning, as described in https://huggingface.co/papers/2508.05629)." ) }, ) activation_offloading: bool = field( default=False, metadata={"help": "Whether to offload the activations to the CPU."}, ) def __post_init__(self): super().__post_init__() if self.packing_strategy == "bfd-requeue": warnings.warn( "The `bfd-requeue` packing strategy has been renamed to `bfd_split`. Please update your configuration accordingly. " "The `bfd-requeue` strategy is deprecated and will be removed in a future version.", FutureWarning, ) self.packing_strategy = "bfd_split" ================================================ FILE: trl/trainer/sft_trainer.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import contextlib import json import os import warnings from collections import defaultdict from collections.abc import Callable from dataclasses import dataclass from pathlib import Path from typing import Any import torch import torch.nn as nn import transformers from accelerate import PartialState from accelerate.logging import get_logger from accelerate.utils import is_peft_model from datasets import Dataset, IterableDataset from packaging.version import Version from transformers import ( AutoProcessor, DataCollator, PreTrainedModel, PreTrainedTokenizerBase, ProcessorMixin, TrainingArguments, ) from transformers.data.data_collator import DataCollatorMixin from transformers.trainer_callback import TrainerCallback from transformers.trainer_utils import EvalPrediction from transformers.utils import is_peft_available from ..chat_template_utils import clone_chat_template from ..data_utils import ( apply_chat_template, is_conversational, is_conversational_from_value, maybe_convert_to_chatml, pack_dataset, prepare_multimodal_messages, truncate_dataset, ) from ..models import get_act_offloading_ctx_manager from .base_trainer import _BaseTrainer from .sft_config import SFTConfig from .utils import ( create_model_from_path, entropy_from_logits, flush_left, get_config_model_id, pad, remove_none_values, selective_log_softmax, ) if is_peft_available(): from peft import PeftConfig, PeftModel, PeftType, get_peft_model logger = get_logger(__name__) FLASH_ATTENTION_VARIANTS = { "flash_attention_2", "flash_attention_3", "kernels-community/flash-attn2", "kernels-community/flash-attn3", "kernels-community/vllm-flash-attn3", } def get_dataset_column_names(dataset: Dataset | IterableDataset) -> list[str]: return list(next(iter(dataset)).keys()) if dataset.column_names is None else dataset.column_names @dataclass class DataCollatorForLanguageModeling(DataCollatorMixin): """ Data collator used for language modeling data. Inputs are dynamically padded to the maximum length of a batch. This collator expects each example in the input list to be a dictionary containing at least the `"input_ids"` key. If the input contains a `"completion_mask"`, it is used to set the labels to `-100` for tokens that are not in the completion. If `"assistant_masks"` are present, they are used to set the labels to `-100` for tokens that are not in the assistant part of the sequence. The collator returns a dictionary containing the following keys: - `"input_ids"`: Tensor of input IDs, padded to the maximum length of the batch. - `"labels"`: Tensor of labels, padded to the maximum length of the batch. If `completion_only_loss` is set to `True`, tokens that are not in the completion are set to -100. If `assistant_masks` are present, tokens that are not in the assistant part of the sequence are set to -100. If `padding_free` is set to `False`, the following key is also returned: - `"attention_mask"`: Tensor of attention masks, padded to the maximum length of the batch. If `padding_free` is set to `True`, the following key is also returned: - `"position_ids"`: Tensor of position IDs, padded to the maximum length of the batch. Args: pad_token_id (`int`): Token ID to use for padding. completion_only_loss (`bool`, *optional*, defaults to `True`): When the input contains a completion mask (`completion_mask`), the labels are set to -100 for the tokens that are no in the completion. padding_free (`bool`, *optional*, defaults to `False`): If set to `True`, the sequences will be flattened into a single sequence, and the position IDs will be generated accordingly and returned instead of the attention mask. pad_to_multiple_of (`int`, *optional*): If set, the sequences will be padded to a multiple of this value. return_tensors (`str`, *optional*, defaults to `"pt"`): Type of Tensor to return. Only `"pt"` is currently supported. Examples: ```python >>> from trl.trainer.sft_trainer import DataCollatorForLanguageModeling >>> collator = DataCollatorForLanguageModeling(pad_token_id=0) >>> examples = [{"input_ids": [1, 2, 3]}, {"input_ids": [4, 5]}] >>> collator(examples) {'input_ids': tensor([[ 1, 2, 3], [ 4, 5, 0]]), 'attention_mask': tensor([[ 1, 1, 1], [ 1, 1, 0]]), 'labels': tensor([[ 1, 2, 3], [ 4, 5, -100]])} >>> # With completion mask >>> examples = [ ... {"input_ids": [1, 2, 3], "completion_mask": [0, 1, 1]}, ... {"input_ids": [4, 5], "completion_mask": [0, 1]}, ... ] >>> collator(examples) {'input_ids': tensor([[ 1, 2, 3], [ 4, 5, 0]]), 'attention_mask': tensor([[ 1, 1, 1], [ 1, 1, 0]]), 'labels': tensor([[-100, 2, 3], [-100, 5, -100]])} >>> # With padding_free >>> collator = DataCollatorForLanguageModeling(pad_token_id=0, padding_free=True) >>> collator(examples) {'input_ids': tensor([[ 1, 2, 3, 4, 5]]), 'position_ids': tensor([[0, 1, 2, 0, 1]]), 'labels': tensor([[1, 2, 3, 4, 5]])} ``` """ pad_token_id: int completion_only_loss: bool = True padding_free: bool = False pad_to_multiple_of: int | None = None return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: # Convert to tensor input_ids = [torch.tensor(example["input_ids"]) for example in examples] if "labels" in examples[0]: labels = [torch.tensor(example["labels"]) for example in examples] else: labels = [torch.tensor(example["input_ids"]) for example in examples] # For padding-free, we should NOT create attention_mask as it causes FlashAttention to ignore position_ids and # compute wrong cu_seq_lens from the all-1s mask if self.padding_free: if "seq_lengths" in examples[0]: position_ids = self.get_position_ids_from_packed_seq_lengths( [example["seq_lengths"] for example in examples] ) else: position_ids = [torch.arange(len(ids)) for ids in input_ids] else: attention_mask = [torch.ones_like(ids) for ids in input_ids] if self.completion_only_loss and "completion_mask" in examples[0]: completion_mask = [torch.tensor(example["completion_mask"]) for example in examples] if "assistant_masks" in examples[0]: assistant_masks = [torch.tensor(example["assistant_masks"]) for example in examples] # If padding_free, flatten everything into a single sequence output = {} if self.padding_free: input_ids = [torch.cat(input_ids, dim=0)] labels = [torch.cat(labels, dim=0)] position_ids = [torch.cat(position_ids, dim=0)] if self.completion_only_loss and "completion_mask" in examples[0]: completion_mask = [torch.cat(completion_mask, dim=0)] if "assistant_masks" in examples[0]: assistant_masks = [torch.cat(assistant_masks, dim=0)] # Pad output["input_ids"] = pad( input_ids, padding_value=self.pad_token_id, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, ) output["labels"] = pad( labels, padding_value=-100, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ) if self.padding_free: output["position_ids"] = pad( position_ids, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ) output["labels"][output["position_ids"] == 0] = -100 else: output["attention_mask"] = pad( attention_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ) if self.completion_only_loss and "completion_mask" in examples[0]: completion_mask = pad( completion_mask, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ) output["labels"][completion_mask == 0] = -100 # mask everything that is not in the completion if "assistant_masks" in examples[0]: assistant_masks = pad( assistant_masks, padding_value=0, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of ) output["labels"][assistant_masks == 0] = -100 return output @staticmethod def get_position_ids_from_packed_seq_lengths(batch_seq_lengths: list[list[int]]) -> list[torch.Tensor]: """ Get position IDs for packed sequences. Args: batch_seq_lengths (`list[list[int]]`): A list of lists containing the lengths of each individual document in the packed batch. Return: `list[torch.Tensor]`: A list of tensors containing the position IDs for each packed sequence. """ # Get lengths per row example_lengths = [sum(seq_lengths) for seq_lengths in batch_seq_lengths] # Flat list of lengths batch_seq_lengths = torch.tensor( [seq_length for seq_lengths in batch_seq_lengths for seq_length in seq_lengths] ) position_ids = torch.ones(sum(example_lengths), dtype=batch_seq_lengths.dtype) position_ids[0] = 0 # Reset position ids to 0 at the start of each sequence position_ids[batch_seq_lengths[:-1].cumsum(0)] = -(batch_seq_lengths[:-1] - 1) position_ids = position_ids.cumsum(0) # Split back into one tensor per example return list(position_ids.split(example_lengths)) @dataclass class DataCollatorForVisionLanguageModeling(DataCollatorMixin): """ Data collator for vision-language modeling tasks. Unlike text-only datasets, where the collator typically receives pre-tokenized inputs ready for batching, vision-language data processing involves converting images into pixel values. This conversion is disk-intensive, making upfront preprocessing of the entire dataset impractical. Therefore, this collator performs tokenization and image processing on-the-fly to efficiently prepare batches. Each input example should be a dictionary containing at least: - An `"images"` key holding a list of images, or an `"image"` key holding a single image. - [language modeling](#language-modeling) type: either a `"messages"` key for conversational inputs or a `"text"` key for standard text inputs. - [prompt-completion](#prompt-completion) type: keys `"prompt"` and `"completion"` for the prompt and completion. The collator outputs a dictionary including: - `"input_ids"`: Tensor of token IDs. - `"attention_mask"`: Tensor indicating attention mask. - `"pixel_values"`: Tensor representing image pixel values. - `"labels"`: Tensor for training labels. Additional keys may be present depending on the processor, such as `"image_grid_thw"`. Args: processor ([`~transformers.ProcessorMixin`]): The processor used to tokenize text and process images. It must be a subclass of [`~transformers.ProcessorMixin`] and include a `tokenizer` with a defined `pad_token_id`. max_length (`int` or `None`, optional, defaults to `None`): Maximum sequence length for input tokens. If `None`, no truncation is applied. completion_only_loss (`bool`, *optional*, defaults to `False`): Whether to compute loss only on the completion part of the sequence. When `True`, the labels for the prompt part are set to -100. It requires the dataset type to be prompt-completion. pad_to_multiple_of (`int` or `None`, optional, defaults to `None`): If set, the sequences will be padded to a multiple of this value. dataset_text_field (`str`, optional, defaults to `"text"`): Name of the column that contains text data in the dataset. This parameter is only relevant for [standard datasets format](dataset_formats#standard). return_tensors (`str`, optional, defaults to `"pt"`): The tensor type to return. Currently, only `"pt"` (PyTorch tensors) is supported. Example: ```python >>> from trl.trainer.sft_trainer import DataCollatorForVisionLanguageModeling >>> from transformers import AutoProcessor >>> processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-7B-Instruct") >>> collator = DataCollatorForVisionLanguageModeling(processor) >>> examples = [ ... {"images": [Image.open("image_0.png")], "messages": [{"role": "user", "content": "What is this?"}]}, ... {"images": [Image.open("image_1.png")], "messages": [{"role": "user", "content": "Describe this image."}]}, ... ] >>> collator(examples) {'input_ids': tensor([[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 3838, 374, 419, 30, 151645, 198], [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 74785, 419, 2168, 13, 151645, 198]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'pixel_values': tensor([[-0.9893, 0.1785, 1.5362, ..., -0.0582, 0.8661, -0.2431], [-0.2302, 0.9522, -1.1061, ..., 0.0555, 1.3354, -0.6412], [ 1.2150, 0.9084, 0.7041, ..., 0.2404, -0.8403, -0.5133], ..., [ 0.6895, 0.2807, 0.2515, ..., -0.2004, -1.2100, 0.0555], [ 0.8209, -0.9748, 1.5654, ..., 1.6055, -0.4706, 0.5817], [-1.0915, 0.4559, 0.9230, ..., 0.5106, 0.0982, -0.1720]]), 'image_grid_thw': tensor([[1, 4, 4], [1, 4, 4]]), 'labels': tensor([[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 3838, 374, 419, 30, 151645, 198], [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 151652, 151655, 151655, 151655, 151655, 151653, 74785, 419, 2168, 13, 151645, 198]])} ``` """ processor: ProcessorMixin max_length: int | None = None completion_only_loss: bool = False # default not used in practice; SFTTrainer always passes the relevant value pad_to_multiple_of: int | None = None dataset_text_field: str = "text" return_tensors: str = "pt" def torch_call(self, examples: list[dict[str, Any]]) -> dict[str, Any]: if "messages" in examples[0] or self.dataset_text_field in examples[0]: if self.completion_only_loss: raise ValueError( "The `completion_only_loss` argument is not supported for language modeling datasets." ) return self._collate_language_modeling(examples) elif "prompt" in examples[0] and "completion" in examples[0]: return self._collate_prompt_completion(examples) else: raise KeyError(f"Unexpected input keys in examples: {list(examples[0].keys())}.") def _collate_language_modeling(self, examples: list[dict[str, Any]]) -> dict[str, Any]: if "image" in examples[0]: for example in examples: example["images"] = [example.pop("image")] images = [example["images"] for example in examples] # Transformers requires at least one image in the batch, otherwise it throws an error if all(img_list == [] for img_list in images): images = None if "messages" in examples[0]: # conversational case messages = [prepare_multimodal_messages(example["messages"], example["images"]) for example in examples] texts = self.processor.apply_chat_template(messages) elif self.dataset_text_field in examples[0]: # standard case texts = [example[self.dataset_text_field] for example in examples] else: raise KeyError( "The input examples must contain either 'messages' for conversational data or 'text' for standard " "data." ) output = self.processor( images=images, text=texts, padding=True, padding_side="right", pad_to_multiple_of=self.pad_to_multiple_of, truncation=self.max_length is not None, max_length=self.max_length, return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) labels = output["input_ids"].clone() labels[output["attention_mask"] == 0] = -100 # We mask only padding tokens (-100) in the labels. Vision tokens are left unchanged because their handling in # loss computation has to be done by the model, and masking them here would be infeasible in practice as vision # token definitions vary across architectures. output["labels"] = labels return output def _collate_prompt_completion(self, examples: list[dict[str, Any]]) -> dict[str, Any]: if self.pad_to_multiple_of is not None: raise NotImplementedError( "Padding to a multiple of a value is not yet implemented for vision-language modeling and " "prompt-completion data." ) if "image" in examples[0]: for example in examples: example["images"] = [example.pop("image")] images = [example["images"] for example in examples] # Transformers requires at least one image in the batch, otherwise it throws an error if all(img_list == [] for img_list in images): images = None if is_conversational(examples[0]): # conversational case for example in examples: example["prompt"] = prepare_multimodal_messages(example["prompt"], images=example["images"]) example["completion"] = prepare_multimodal_messages(example["completion"], images=[]) examples = [apply_chat_template(example, self.processor) for example in examples] prompts = [example["prompt"] for example in examples] completions = [example["completion"] for example in examples] processed_prompts = self.processor( images=images, text=prompts, padding=True, padding_side="left", return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) processed_completions = self.processor( text=completions, padding=True, padding_side="right", return_tensors=self.return_tensors, add_special_tokens=False, # to avoid adding the BOS, twice see https://huggingface.co/blog/qgallouedec/gotchas-in-tokenizer-behavior#7-chat-template-and-tokenization-dont-compose-due-to-special-tokens ) # Concatenate prompts and completions prompt_ids, prompt_mask = processed_prompts["input_ids"], processed_prompts["attention_mask"] completion_ids, completion_mask = processed_completions["input_ids"], processed_completions["attention_mask"] input_ids = torch.cat((prompt_ids, completion_ids), dim=1) attention_mask = torch.cat((prompt_mask, completion_mask), dim=1) completion_mask = torch.cat((torch.zeros_like(prompt_mask), completion_mask), dim=1) if "token_type_ids" in processed_prompts: # special case for Gemma prompt_token_type_ids = processed_prompts["token_type_ids"] completion_token_type_ids = processed_completions["token_type_ids"] token_type_ids = torch.cat((prompt_token_type_ids, completion_token_type_ids), dim=1) if "mm_token_type_ids" in processed_prompts: # special case for ERNIE-VL prompt_mm_token_type_ids = processed_prompts["mm_token_type_ids"] completion_mm_token_type_ids = processed_completions.get( "mm_token_type_ids", torch.zeros_like(completion_ids) ) mm_token_type_ids = torch.cat((prompt_mm_token_type_ids, completion_mm_token_type_ids), dim=1) # Flush left to reduce padding if "token_type_ids" in processed_prompts and "mm_token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, token_type_ids, mm_token_type_ids = flush_left( attention_mask, input_ids, completion_mask, token_type_ids, mm_token_type_ids ) elif "token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, token_type_ids = flush_left( attention_mask, input_ids, completion_mask, token_type_ids ) elif "mm_token_type_ids" in processed_prompts: attention_mask, input_ids, completion_mask, mm_token_type_ids = flush_left( attention_mask, input_ids, completion_mask, mm_token_type_ids ) else: attention_mask, input_ids, completion_mask = flush_left(attention_mask, input_ids, completion_mask) # Truncate if necessary if self.max_length is not None: input_ids = input_ids[:, : self.max_length] attention_mask = attention_mask[:, : self.max_length] completion_mask = completion_mask[:, : self.max_length] if "token_type_ids" in processed_prompts: token_type_ids = token_type_ids[:, : self.max_length] if "mm_token_type_ids" in processed_prompts: mm_token_type_ids = mm_token_type_ids[:, : self.max_length] # Create labels and mask padding tokens labels = input_ids.clone() labels[attention_mask == 0] = -100 if self.completion_only_loss: labels[completion_mask == 0] = -100 # Build the output dictionary output = processed_prompts # we take processed_prompts because it contains the images output["input_ids"] = input_ids output["attention_mask"] = attention_mask output["labels"] = labels if "token_type_ids" in processed_prompts: output["token_type_ids"] = token_type_ids if "mm_token_type_ids" in processed_prompts: output["mm_token_type_ids"] = mm_token_type_ids return output def dft_loss(outputs, labels, num_items_in_batch=None): """ DFT loss function, as presented in [On the Generalization of SFT: A Reinforcement Learning Perspective with Reward Rectification](https://huggingface.co/papers/2508.05629) """ labels = nn.functional.pad(labels, (0, 1), value=-100) shift_labels = labels[..., 1:].contiguous() loss_mask = shift_labels != -100 shift_labels[~loss_mask] = 0 logprobs = selective_log_softmax(outputs.logits, shift_labels) per_token_loss = -logprobs.exp().detach() * logprobs if num_items_in_batch is None: num_items_in_batch = loss_mask.sum() loss = (per_token_loss * loss_mask).sum() / num_items_in_batch return loss class SFTTrainer(_BaseTrainer): """ Trainer for Supervised Fine-Tuning (SFT) method. This class is a wrapper around the [`~transformers.Trainer`] class and inherits all of its attributes and methods. Example: ```python from trl import SFTTrainer from datasets import load_dataset dataset = load_dataset("roneneldan/TinyStories", split="train[:1%]") trainer = SFTTrainer( model="Qwen/Qwen2.5-0.5B-Instruct", train_dataset=dataset, ) trainer.train() ``` Args: model (`str` or [`~transformers.PreTrainedModel`] or [`~peft.PeftModel`]): Model to be trained. Can be either: - A string, being the *model id* of a pretrained model hosted inside a model repo on huggingface.co, or a path to a *directory* containing model weights saved using [`~transformers.PreTrainedModel.save_pretrained`], e.g., `'./my_model_directory/'`. The model is loaded using `.from_pretrained` (where `` is derived from the model config) with the keyword arguments in `args.model_init_kwargs`. - A [`~transformers.PreTrainedModel`] object. Only causal language models are supported. - A [`~peft.PeftModel`] object. Only causal language models are supported. If you're training a model with an MoE architecture and want to include the load balancing/auxiliary loss as a part of the final loss, remember to set the `output_router_logits` config of the model to `True`. args ([`SFTConfig`], *optional*): Configuration for this trainer. If `None`, a default configuration is used. data_collator ([`~transformers.DataCollator`], *optional*): Function to use to form a batch from a list of elements of the processed `train_dataset` or `eval_dataset`. Will default to [`~trainer.sft_trainer.DataCollatorForLanguageModeling`] if the model is a language model and [`~trainer.sft_trainer.DataCollatorForVisionLanguageModeling`] if the model is a vision-language model. train_dataset ([`~datasets.Dataset`] or [`~datasets.IterableDataset`]): Dataset to use for training. This trainer supports both [language modeling](#language-modeling) type and [prompt-completion](#prompt-completion) type. The format of the samples can be either: - [Standard](dataset_formats#standard): Each sample contains plain text. - [Conversational](dataset_formats#conversational): Each sample contains structured messages (e.g., role and content). The trainer also supports processed datasets (tokenized) as long as they contain an `input_ids` field. eval_dataset ([`~datasets.Dataset`], [`~datasets.IterableDataset`] or `dict[str, Dataset | IterableDataset]`): Dataset to use for evaluation. It must meet the same requirements as `train_dataset`. processing_class ([`~transformers.PreTrainedTokenizerBase`], [`~transformers.ProcessorMixin`], *optional*): Processing class used to process the data. If `None`, the processing class is loaded from the model's name with [`~transformers.AutoProcessor.from_pretrained`]. A padding token, `tokenizer.pad_token`, must be set. If the processing class has not set a padding token, `tokenizer.eos_token` will be used as the default. compute_loss_func (`Callable`, *optional*): A function that accepts the raw model outputs, labels, and the number of items in the entire accumulated batch (batch_size * gradient_accumulation_steps) and returns the loss. For example, see the default [loss function](https://github.com/huggingface/transformers/blob/052e652d6d53c2b26ffde87e039b723949a53493/src/transformers/trainer.py#L3618) used by [`Trainer`]. compute_metrics (`Callable[[EvalPrediction], dict]`, *optional*): The function that will be used to compute metrics at evaluation. Must take a [`~transformers.EvalPrediction`] and return a dictionary string to metric values. When passing [`SFTConfig`] with `batch_eval_metrics` set to `True`, your `compute_metrics` function must take a boolean `compute_result` argument. This will be triggered after the last eval batch to signal that the function needs to calculate and return the global summary statistics rather than accumulating the batch-level statistics. callbacks (list of [`~transformers.TrainerCallback`], *optional*): List of callbacks to customize the training loop. Will add those to the list of default callbacks detailed in [here](https://huggingface.co/docs/transformers/main_classes/callback). If you want to remove one of the default callbacks used, use the [`~transformers.Trainer.remove_callback`] method. optimizers (`tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None]`, *optional*, defaults to `(None, None)`): A tuple containing the optimizer and the scheduler to use. Will default to an instance of `AdamW` on your model and a scheduler given by [`~transformers.get_linear_schedule_with_warmup`] controlled by `args`. optimizer_cls_and_kwargs (`tuple[Type[torch.optim.Optimizer], Dict[str, Any]]`, *optional*): A tuple containing the optimizer class and keyword arguments to use. Overrides `optim` and `optim_args` in `args`. Incompatible with the `optimizers` argument. Unlike `optimizers`, this argument avoids the need to place model parameters on the correct devices before initializing the Trainer. preprocess_logits_for_metrics (`Callable[[torch.Tensor, torch.Tensor], torch.Tensor]`, *optional*): A function that preprocess the logits right before caching them at each evaluation step. Must take two tensors, the logits and the labels, and return the logits once processed as desired. The modifications made by this function will be reflected in the predictions received by `compute_metrics`. Note that the labels (second parameter) will be `None` if the dataset does not have them. peft_config ([`~peft.PeftConfig`], *optional*): PEFT configuration used to wrap the model. If `None`, the model is not wrapped. formatting_func (`Callable`, *optional*): Formatting function applied to the dataset before tokenization. Applying the formatting function explicitly converts the dataset into a [language modeling](#language-modeling) type. """ _tag_names = ["trl", "sft"] _name = "SFT" def __init__( self, model: "str | PreTrainedModel | PeftModel", args: SFTConfig | TrainingArguments | None = None, data_collator: DataCollator | None = None, train_dataset: Dataset | IterableDataset | None = None, eval_dataset: Dataset | IterableDataset | dict[str, Dataset | IterableDataset] | None = None, processing_class: PreTrainedTokenizerBase | ProcessorMixin | None = None, compute_loss_func: Callable | None = None, compute_metrics: Callable[[EvalPrediction], dict] | None = None, callbacks: list[TrainerCallback] | None = None, optimizers: tuple[torch.optim.Optimizer | None, torch.optim.lr_scheduler.LambdaLR | None] = (None, None), optimizer_cls_and_kwargs: tuple[type[torch.optim.Optimizer], dict[str, Any]] | None = None, preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] | None = None, peft_config: "PeftConfig | None" = None, formatting_func: Callable[[dict], str] | None = None, ): # Args if args is None: model_name = model if isinstance(model, str) else get_config_model_id(model.config) model_name = model_name.split("/")[-1] args = SFTConfig(f"{model_name}-SFT") elif isinstance(args, TrainingArguments) and not isinstance(args, SFTConfig): dict_args = args.to_dict() dict_args["hub_token"] = args.hub_token # to_dict hides the hub_token if Version(transformers.__version__) < Version("5.0.0"): dict_args.pop("push_to_hub_token") args = SFTConfig(**dict_args) if train_dataset is None: raise ValueError("`train_dataset` is required") elif isinstance(train_dataset, IterableDataset): # IterableDataset requires dispatch_batches=False because Accelerate's dispatch mode may try to concatenate # batches from multiple processes, leading to mismatch errors. if args.accelerator_config.dispatch_batches is True: logger.warning( "You are using an `IterableDataset` for training with `dispatch_batches=True`. `dispatch_batches` " "is forced to `False` when using an `IterableDataset`. To remove this warning, unset " "`dispatch_batches` in `SFTConfig` or set it to `False`." ) args.accelerator_config.dispatch_batches = False # Model if isinstance(model, str): model_init_kwargs = args.model_init_kwargs or {} # Distributed training requires device_map=None ("auto" fails) if args.distributed_state.distributed_type in ["MULTI_GPU", "DEEPSPEED"]: model_init_kwargs["device_map"] = None model = create_model_from_path(model, **model_init_kwargs) else: if args.model_init_kwargs is not None: logger.warning( "You passed `model_init_kwargs` to the `SFTConfig`, but your model is already instantiated. " "The `model_init_kwargs` will be ignored." ) # Processing class if processing_class is None: processing_class = AutoProcessor.from_pretrained(get_config_model_id(model.config)) # Handle pad token for processors or tokenizers if isinstance(processing_class, ProcessorMixin): tokenizer = processing_class.tokenizer self._is_vlm = True elif isinstance(processing_class, PreTrainedTokenizerBase): tokenizer = processing_class self._is_vlm = False else: raise TypeError("The `processing_class` must be either a `PreTrainedTokenizerBase` or a `ProcessorMixin`") if args.eos_token is not None: eos_token = args.eos_token eos_token_id = tokenizer.convert_tokens_to_ids(eos_token) if eos_token_id is None: raise ValueError( f"The specified `eos_token` ('{eos_token}') is not found in the vocabulary of the given " f"`processing_class` ({processing_class.__class__.__name__}). Ensure that the `eos_token` exists " "in the vocabulary before using it as an EOS token." ) tokenizer.eos_token_id = eos_token_id if args.chat_template_path is not None: if os.path.isfile(args.chat_template_path) and args.chat_template_path.endswith((".jinja", ".j2")): with open(args.chat_template_path, encoding="utf-8") as chat_template_file: processing_class.chat_template = chat_template_file.read() added_tokens = [] else: model, processing_class, added_tokens = clone_chat_template( model, processing_class, args.chat_template_path ) else: added_tokens = [] # Catch some wrong configurations related to VLMs if self._is_vlm and args.packing: raise ValueError( "Packing is not supported for vision-language models. Please set `packing=False` in the SFTConfig." ) if self._is_vlm and args.padding_free: raise ValueError( "Padding-free training is yet not supported for vision-language models. Please set " "`padding_free=False` in the `SFTConfig`." ) if self._is_vlm and args.assistant_only_loss: raise ValueError( "Assistant-only loss is not yet supported for vision-language models. Please set " "`assistant_only_loss=False` in the `SFTConfig`." ) if self._is_vlm and args.max_length is not None and args.truncation_mode == "keep_end": raise ValueError( "truncation_mode='keep_end' is not supported for vision-language models. Image tokens reside " "inside the prompt portion of the sequence; depending on the example, keep_end may silently " "drop them, causing pixel_values to be forwarded to the model with no corresponding visual " "tokens in input_ids. Use truncation_mode='keep_start' (the default) or set max_length=None." ) # PEFT configuration and model wrapping if peft_config is not None: if added_tokens: # Ensure that the added tokens are trainable if peft_config.trainable_token_indices is None: peft_config.trainable_token_indices = {"embed_tokens": added_tokens} elif "embed_tokens" not in peft_config.trainable_token_indices: peft_config.trainable_token_indices["embed_tokens"] = added_tokens else: peft_config.trainable_token_indices["embed_tokens"].extend(added_tokens) # Ensure that the lm_head is trainable if peft_config.modules_to_save is None or "lm_head" not in peft_config.modules_to_save: logger.warning( "Cloning chat template added new tokens to the tokenizer, but 'lm_head' is not in PEFT's " "`modules_to_save`. As a result, the model may not learn to generate outputs with these new " "tokens, leading to degraded generation quality. To fix this, add " "`modules_to_save=['lm_head']` to your PEFT configuration." ) if peft_config.modules_to_save is None: peft_config.modules_to_save = ["lm_head"] else: peft_config.modules_to_save.append("lm_head") if is_peft_available() and is_peft_model(model) and peft_config is not None: raise ValueError( "You passed a `PeftModel` instance together with a `peft_config` to the trainer. Please first merge " "and unload the existing adapter, save the resulting base model, and then pass that base model along " "with the new `peft_config` to the trainer." ) # Create PEFT model if peft_config is not None: model = get_peft_model(model, peft_config) # PEFT + DeepSpeed ZeRO-3 requires reentrant checkpointing. For more details, see # https://github.com/huggingface/trl/issues/2514#issuecomment-2692152703 if ( is_peft_model(model) and args.deepspeed_plugin is not None and args.deepspeed_plugin.zero_stage == 3 and args.gradient_checkpointing ): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} use_reentrant = args.gradient_checkpointing_kwargs.get("use_reentrant") if use_reentrant is False: logger.warning( "You are using PEFT with DeepSpeed ZeRO-3 and gradient checkpointing with `use_reentrant=False`. " "`use_reentrant` is forced to `True` in this configuration to ensure correct training. To remove " "this warning, unset `use_reentrant` in `gradient_checkpointing_kwargs` or set it to `True`." ) args.gradient_checkpointing_kwargs["use_reentrant"] = True # When using gradient checkpointing with PEFT, we need to enable input gradients. transformers.Trainer normally # handles this, but a bug currently prevents it; see https://github.com/huggingface/transformers/issues/42489 if is_peft_available() and is_peft_model(model) and args.gradient_checkpointing: model.enable_input_require_grads() # When using QLoRA, the PEFT adapter weights are converted to bf16 to follow the recommendations from the # original paper (see https://huggingface.co/papers/2305.14314, paragraph 3). Normally, this can be done by # passing `autocast_adapter_dtype=False` to `get_peft_model`, but this option is not yet supported for # quantized models. See: https://github.com/huggingface/peft/issues/2889 # Non-quantized models do not have the `is_loaded_in_{8,4}bit` attributes, whereas quantized models do if getattr(model, "is_loaded_in_4bit", False) or getattr(model, "is_loaded_in_8bit", False): for param in model.parameters(): if param.requires_grad: param.data = param.data.to(torch.bfloat16) # In Prompt Tuning a small set of trainable virtual tokens (continuous prompt embeddings) is prepended to the # input. We store the number of these tokens so we can account for them correctly when calculating accuracy. self.num_virtual_tokens = 0 if is_peft_available() and is_peft_model(model): if model.active_adapter in model.peft_config: peft_model_config = model.peft_config[model.active_adapter] self.num_virtual_tokens = getattr(peft_model_config, "num_virtual_tokens", 0) # Data collator # BFD packing requires padding-free mode; otherwise, the collator outputs padded attention masks, causing # FlashAttention to ignore position_ids and recompute them incorrectly from the padded attention mask. self.padding_free = args.padding_free or (args.packing and args.packing_strategy in {"bfd", "bfd_split"}) use_flash_attention = model.config._attn_implementation in FLASH_ATTENTION_VARIANTS if self.padding_free: if data_collator is not None: raise ValueError("Passing a custom data collator is not supported when using padding-free.") if args.packing and args.packing_strategy == "wrapped": logger.warning( "You are passing `padding_free=True` with the 'wrapped' packing strategy, which is not " "recommended. Please refer to the documentation to understand why this is not recommended." ) if not use_flash_attention: logger.warning( "Padding-free training is enabled, but the attention implementation is not set to a supported " "flash attention variant. Padding-free training flattens batches into a single sequence, and only " "the following implementations are known to reliably support this: " f"{', '.join(sorted(FLASH_ATTENTION_VARIANTS))}. Using other implementations may lead to " "unexpected behavior. To ensure compatibility, set `attn_implementation` in the model " "configuration to one of these supported options or verify that your attention mechanism can " "handle flattened sequences." ) if args.per_device_train_batch_size == 1 and not args.packing: logger.warning( "You are using a per_device_train_batch_size of 1 with padding-free training. Using a batch size " "of 1 annihilate the benefits of padding-free training. Please consider increasing the batch size " "to at least 2." ) # Decide whether to use completion-only loss: if not specified, then it is set to True if the dataset format # is prompt-completion, and False if the dataset format is language modeling. dataset_sample = next(iter(train_dataset)) if args.completion_only_loss is None: self.completion_only_loss = "prompt" in dataset_sample and "completion" in dataset_sample else: self.completion_only_loss = args.completion_only_loss self._is_vision_dataset = "image" in dataset_sample or "images" in dataset_sample if self._is_vision_dataset and not self._is_vlm: raise ValueError( "The dataset appears to be vision-related (contains 'image' or 'images' keys), but the provided " "model does not seem to be a vision-language model. Please check your model and dataset." ) if data_collator is None and not self._is_vision_dataset: # Get the pad token: if not provided, use the one from the processing class or the eos token # if the processing class does not have a pad token. pad_token = args.pad_token or tokenizer.pad_token or tokenizer.eos_token pad_token_id = tokenizer.convert_tokens_to_ids(pad_token) if pad_token_id is None: raise ValueError( f"The specified `pad_token` ('{pad_token}') is not found in the vocabulary of the given " f"`processing_class` ({processing_class.__class__.__name__}). Ensure that the `pad_token` exists " "in the vocabulary before using it as a padding token." ) data_collator = DataCollatorForLanguageModeling( pad_token_id=pad_token_id, completion_only_loss=self.completion_only_loss, padding_free=self.padding_free, pad_to_multiple_of=args.pad_to_multiple_of, ) elif data_collator is None and self._is_vision_dataset: data_collator = DataCollatorForVisionLanguageModeling( processor=processing_class, max_length=args.max_length, completion_only_loss=self.completion_only_loss, pad_to_multiple_of=args.pad_to_multiple_of, dataset_text_field=args.dataset_text_field, ) if args.packing and args.packing_strategy in {"bfd", "bfd_split"} and not use_flash_attention: logger.warning( "You are using packing, but the attention implementation is not set to a supported flash attention " "variant. Packing gathers multiple samples into a single sequence, and only the following " f"implementations are known to reliably support this: {', '.join(sorted(FLASH_ATTENTION_VARIANTS))}. " "Using other implementations may lead to cross-contamination between samples. To avoid this, either " "disable packing by setting `packing=False`, or set `attn_implementation` in the model configuration " "to one of these supported options." ) if args.assistant_only_loss and not is_conversational(dataset_sample): raise ValueError( "You set `assistant_only_loss=True`, but the dataset is not conversational. This option is only " "supported for conversational datasets." ) # Dataset # Skip dataset preparation if `skip_prepare_dataset=True` in `dataset_kwargs`, or if it's a VLM, where # preprocessing (e.g., image-to-pixel conversion) is too costly and done on the fly instead. skip_prepare_dataset = ( args.dataset_kwargs is not None and args.dataset_kwargs.get("skip_prepare_dataset", False) or self._is_vision_dataset ) if not skip_prepare_dataset: if self.completion_only_loss and formatting_func: raise ValueError( "A formatting function was provided while `completion_only_loss=True`, which is incompatible. " "Using a formatter converts the dataset to a language modeling type, conflicting with " "completion-only loss. To resolve this, apply your formatting function before passing the " "dataset, or disable `completion_only_loss` in `SFTConfig`." ) train_dataset = self._prepare_dataset( train_dataset, processing_class, args, args.packing, formatting_func, "train" ) if eval_dataset is not None: packing = args.packing if args.eval_packing is None else args.eval_packing if isinstance(eval_dataset, dict): eval_dataset = { key: self._prepare_dataset(dataset, processing_class, args, packing, formatting_func, key) for key, dataset in eval_dataset.items() } else: eval_dataset = self._prepare_dataset( eval_dataset, processing_class, args, packing, formatting_func, "eval" ) # Loss function if not args.use_liger_kernel: # liger supports dft loss by just passing use_token_scaling=True if args.loss_type == "nll": pass # use the default loss elif args.loss_type == "dft": if compute_loss_func is not None: raise ValueError( "You passed a `compute_loss_func` together with `loss_type='dft'` to the `SFTTrainer`. " "When using `loss_type='dft'`, the loss function is internally set to the DFT loss, so " "passing a `compute_loss_func` is not allowed." ) compute_loss_func = dft_loss else: raise ValueError(f"Invalid `loss_type` {args.loss_type} passed. Supported values are 'nll' and 'dft'.") # Transformers explicitly set use_reentrant=True in the past to silence a PyTorch warning, but the default was # never updated once PyTorch switched to recommending use_reentrant=False. Until that change lands upstream # (see https://github.com/huggingface/transformers/pull/43203) and is released (most likely in 5.0.0), we # default to the recommended non-reentrant behavior here, while preserving any user-provided value. if args.gradient_checkpointing and Version(transformers.__version__) < Version("5.0.0"): args.gradient_checkpointing_kwargs = args.gradient_checkpointing_kwargs or {} args.gradient_checkpointing_kwargs.setdefault("use_reentrant", False) super().__init__( model=model, args=args, data_collator=data_collator, train_dataset=train_dataset, eval_dataset=eval_dataset, processing_class=processing_class, compute_loss_func=compute_loss_func, compute_metrics=compute_metrics, callbacks=callbacks, optimizers=optimizers, optimizer_cls_and_kwargs=optimizer_cls_and_kwargs, preprocess_logits_for_metrics=preprocess_logits_for_metrics, ) # Initialize activation offloading context if self.args.activation_offloading: self.maybe_activation_offload_context = get_act_offloading_ctx_manager(model=self.model) else: self.maybe_activation_offload_context = contextlib.nullcontext() self.aux_loss_enabled = getattr(model.config, "output_router_logits", False) # Initialize the metrics self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)} self._total_train_tokens = 0 # Add tags to the model self.model.add_model_tags(self._tag_names) def _prepare_dataset( self, dataset: Dataset | IterableDataset, processing_class: PreTrainedTokenizerBase | ProcessorMixin, args: SFTConfig, packing: bool, formatting_func: Callable[[dict], str] | None, dataset_name: str, ) -> Dataset | IterableDataset: # Tabular backends like Arrow/Parquet insert `None` for mismatched keys in nested structures. Clean them from # sampled data. if isinstance(dataset, Dataset): # IterableDataset does not support `with_transform` dataset = dataset.with_transform(remove_none_values) # If the dataset is already preprocessed (tokenized), skip the processing steps. column_names = get_dataset_column_names(dataset) is_processed = "input_ids" in column_names # Build the kwargs for the `map` function map_kwargs = {} if isinstance(dataset, Dataset): # IterableDataset does not support num_proc map_kwargs["num_proc"] = args.dataset_num_proc with PartialState().main_process_first(): # Apply the formatting function if any if formatting_func is not None and is_processed: logger.warning( "You passed a dataset that is already processed (contains an `input_ids` field) together with a " "formatting function. Therefore `formatting_func` will be ignored. Either remove the " "`formatting_func` or pass a dataset that is not already processed.", ) if formatting_func is not None and not is_processed: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Applying formatting function to {dataset_name} dataset" def _func(example): return {"text": formatting_func(example)} dataset = dataset.map(_func, batched=False, **map_kwargs) if not is_processed: # Convert the dataset to ChatML if needed first_example = next(iter(dataset)) if is_conversational_from_value(first_example): if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Converting {dataset_name} dataset to ChatML" column_names = get_dataset_column_names(dataset) dataset = dataset.map( maybe_convert_to_chatml, remove_columns="conversations" if "conversations" in column_names else None, **map_kwargs, ) # Apply the chat template if needed first_example = next(iter(dataset)) if not is_conversational(first_example): if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Adding EOS to {dataset_name} dataset" def add_eos(example, eos_token): if "text" in example and not example["text"].endswith(eos_token): # language modeling case example["text"] = example["text"] + eos_token elif "completion" in example and not example["completion"].endswith(eos_token): example["completion"] = example["completion"] + eos_token return example eos_token = processing_class.tokenizer.eos_token if self._is_vlm else processing_class.eos_token dataset = dataset.map( add_eos, fn_kwargs={"eos_token": eos_token}, remove_columns="messages" if "messages" in column_names else None, # renamed to "text" **map_kwargs, ) # Tokenize the dataset if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset" def tokenize_fn(example, processing_class, dataset_text_field, assistant_only_loss): tools = example.get("tools") tools = json.loads(tools) if isinstance(tools, str) else tools if "prompt" in example: # prompt-completion case output = {} if is_conversational(example): if self._is_vlm: prompt = prepare_multimodal_messages(example["prompt"], images=[]) completion = prepare_multimodal_messages(example["completion"], images=[]) else: prompt = example["prompt"] completion = example["completion"] prompt_ids = processing_class.apply_chat_template( prompt, tools=tools, add_generation_prompt=True, tokenize=True, return_dict=False, **example.get("chat_template_kwargs", {}), ) # Fix transformers inconsistency: for VLMs, apply_chat_template returns lists of lists # even for single examples, while for LLMs it returns lists of ints. prompt_ids = prompt_ids[0] if isinstance(prompt_ids[0], list) else prompt_ids prompt_completion_processed = processing_class.apply_chat_template( prompt + completion, tools=tools, tokenize=True, return_dict=True, return_assistant_tokens_mask=assistant_only_loss, **example.get("chat_template_kwargs", {}), ) # Fix transformers inconsistency: for VLMs, apply_chat_template returns lists of lists # even for single examples, while for LLMs it returns lists of ints. prompt_completion_processed = { k: v[0] if isinstance(v[0], list) else v for k, v in prompt_completion_processed.items() } prompt_completion_ids = prompt_completion_processed["input_ids"] if "assistant_masks" in prompt_completion_processed: output["assistant_masks"] = prompt_completion_processed["assistant_masks"] else: prompt_ids = processing_class(text=example["prompt"])["input_ids"] prompt_completion_ids = processing_class(text=example["prompt"] + example["completion"])[ "input_ids" ] # Fix transformers inconsistency: for VLMs, processing_class returns lists of lists # even for single examples, while for LLMs it returns lists of ints. prompt_ids = prompt_ids[0] if isinstance(prompt_ids[0], list) else prompt_ids prompt_completion_ids = ( prompt_completion_ids[0] if isinstance(prompt_completion_ids[0], list) else prompt_completion_ids ) # Check if the tokenized prompt starts with the tokenized prompt+completion if not prompt_completion_ids[: len(prompt_ids)] == prompt_ids: logger.warning( "Mismatch between tokenized prompt and the start of tokenized prompt+completion. " "This may be due to unexpected tokenizer behavior, whitespace issues, or special " "token handling. Verify that the tokenizer is processing text consistently." ) # Create completion mask completion_mask = [0] * len(prompt_ids) + [1] * (len(prompt_completion_ids) - len(prompt_ids)) output["input_ids"] = prompt_completion_ids output["completion_mask"] = completion_mask else: # language modeling case if is_conversational(example): if self._is_vlm: messages = prepare_multimodal_messages(example["messages"], images=[]) else: messages = example["messages"] processed = processing_class.apply_chat_template( messages, tools=tools, tokenize=True, return_dict=True, return_assistant_tokens_mask=assistant_only_loss, **example.get("chat_template_kwargs", {}), ) # Fix transformers inconsistency: for VLMs, apply_chat_template returns lists of lists # even for single examples, while for LLMs it returns lists of ints. processed = {k: v[0] if isinstance(v[0], list) else v for k, v in processed.items()} output = {k: processed[k] for k in ("input_ids", "assistant_masks") if k in processed} else: output = {"input_ids": processing_class(text=example[dataset_text_field])["input_ids"]} if "assistant_masks" in output and 1 not in output["assistant_masks"]: raise RuntimeError( "You're using `assistant_only_loss=True`, but at least one example has no assistant " "tokens. This usually means the tokenizer's chat template doesn't generate assistant " "masks — it may be missing the `{% generation %}` keyword. Please check the template and " "ensure it's correctly configured to support assistant masking." ) return output dataset = dataset.map( tokenize_fn, fn_kwargs={ "processing_class": processing_class, "dataset_text_field": args.dataset_text_field, "assistant_only_loss": args.assistant_only_loss, }, **map_kwargs, ) # Pack or truncate if packing: if args.max_length is None: raise ValueError("When packing is enabled, `max_length` can't be `None`.") if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Packing {dataset_name} dataset" columns = ["input_ids"] if "completion_mask" in get_dataset_column_names(dataset): columns.append("completion_mask") if "assistant_masks" in get_dataset_column_names(dataset): columns.append("assistant_masks") dataset = dataset.select_columns(columns) # Shuffle the dataset before packing. When using wrapped packing, it's important to shuffle before # packing as well to avoid correlations between sequences packed together. if args.shuffle_dataset: dataset = dataset.shuffle(seed=args.seed) # Packing adds new column "seq_lengths" needed for document aware FlashAttention dataset = pack_dataset(dataset, args.max_length, args.packing_strategy, map_kwargs) elif args.max_length is not None: if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc` map_kwargs["desc"] = f"Truncating {dataset_name} dataset" dataset = truncate_dataset( dataset, args.max_length, truncation_mode=args.truncation_mode, map_kwargs=map_kwargs ) # For Liger kernel, ensure only the essential columns if args.use_liger_kernel: collator_expected_keys = {"input_ids", "seq_lengths", "completion_mask", "assistant_masks"} column_names = get_dataset_column_names(dataset) dataset = dataset.select_columns(collator_expected_keys.intersection(column_names)) if args.shuffle_dataset: dataset = dataset.shuffle(seed=args.seed) return dataset def _set_signature_columns_if_needed(self): # If `self.args.remove_unused_columns` is True, non-signature columns are removed. # By default, this method sets `self._signature_columns` to the model's expected inputs (usually, "input_ids" # and "attention_mask"). When using `train_on_completion_only` we add a "completion_mask" column to the # dataset. So we need to override the default signature columns to include "completion_mask" as well. if self._signature_columns is None: if self._is_vision_dataset: self._signature_columns = ["messages", "prompt", "completion", "image", "images"] else: self._signature_columns = ["input_ids", "labels", "seq_lengths", "completion_mask", "assistant_masks"] def compute_loss(self, model, inputs, return_outputs=False, num_items_in_batch=None): mode = "train" if self.model.training else "eval" prediction_loss_only = inputs.pop("_prediction_loss_only", None) # Set aside labels as it will be dropped by super().compute_loss() if a custom `compute_loss_func` is used. # This can be removed when this issue is fixed. # When using CP or SP, labels are pre-shifted, we must use shift_labels instead. labels = inputs["labels"] if "shift_labels" not in inputs else None # If not set, defaults from model config and may warn since cache isn't compatible with gradient checkpointing inputs["use_cache"] = False # Request token accuracy from Liger kernel and set token scaling if using DFT loss if self.args.use_liger_kernel: # Avoid materializing full logits during eval unless explicitly needed. # By default, liger kernel only skips logits during training (self.training=True). # When only loss is needed for eval (no compute_metrics), we can safely skip logits. # prediction_step communicates whether logits are expected via `_prediction_loss_only`; # this prevents skipping logits during `predict()` where outputs are requested. # Keep logits when preprocess_logits_for_metrics is set, even if compute_metrics is None. # to prevent massive vRAM spikes from the lm_head projection. # See: https://github.com/huggingface/trl/issues/4679 inputs["skip_logits"] = ( self.model.training or self.args.prediction_loss_only or ( self.compute_metrics is None and self.preprocess_logits_for_metrics is None and prediction_loss_only is not False ) ) inputs["return_token_accuracy"] = True inputs["use_token_scaling"] = self.args.loss_type == "dft" (loss, outputs) = super().compute_loss( model, inputs, return_outputs=True, num_items_in_batch=num_items_in_batch ) # Compute entropy if not self.args.use_liger_kernel: # liger doesn't return logits with torch.no_grad(): per_token_entropy = entropy_from_logits(outputs.logits) # When using Prompt Tuning, skip the virtual tokens in logits before entropy computation, since they # do not correspond to actual input tokens. if ( self.num_virtual_tokens > 0 and model.peft_config[model.active_adapter].peft_type != PeftType.PREFIX_TUNING ): per_token_entropy = per_token_entropy[:, self.num_virtual_tokens :] if "attention_mask" in inputs: attention_mask = inputs["attention_mask"] entropy = torch.sum(per_token_entropy * attention_mask) / attention_mask.sum() elif "position_ids" in inputs: entropy = torch.mean(per_token_entropy) else: raise ValueError("Expected 'attention_mask' or 'position_ids' in inputs.") entropy = self.accelerator.gather_for_metrics(entropy).mean().item() self._metrics[mode]["entropy"].append(entropy) if mode == "train": # When using padding-free, the attention_mask is not present in the inputs, instead we have cu_seq_lens_q, # cu_seq_lens_k, and max_length_k, max_length_q and position_ids. if "attention_mask" in inputs: num_tokens_in_batch = self.accelerator.gather_for_metrics(inputs["attention_mask"].sum()).sum().item() elif "position_ids" in inputs: local_num_tokens = torch.tensor(inputs["position_ids"].size(1), device=inputs["position_ids"].device) num_tokens_in_batch = self.accelerator.gather_for_metrics(local_num_tokens).sum().item() else: raise ValueError("Expected 'attention_mask' or 'position_ids' in inputs.") self._total_train_tokens += num_tokens_in_batch self._metrics[mode]["num_tokens"] = [self._total_train_tokens] if self.args.use_liger_kernel: if hasattr(outputs, "token_accuracy") and outputs.token_accuracy is not None: token_accuracy = self.accelerator.gather_for_metrics(outputs.token_accuracy).mean().item() self._metrics[mode]["mean_token_accuracy"].append(token_accuracy) else: warnings.warn( "liger-kernel did not return token_accuracy when requested. The mean_token_accuracy metric will " "not be logged. This is unexpected; please report it to the liger-kernel repository.", stacklevel=2, ) else: # Compute accuracy from logits using argmax (traditional method) with torch.no_grad(): if "shift_labels" in inputs: # When using CP or SP, labels are pre-shifted. We must use these (and cannot manually shift) because: # - The first discarded token from inputs["labels"] actually belongs to process n-1 # - The last logits require the label from process n+1 shift_logits = outputs.logits.contiguous() shift_labels = inputs["shift_labels"] else: shift_logits = outputs.logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Prompt Tuning and P-Tuning output logits for virtual tokens but Prefix-Tuning does not. if ( self.num_virtual_tokens > 0 and model.peft_config[model.active_adapter].peft_type != PeftType.PREFIX_TUNING ): shift_logits = shift_logits[:, self.num_virtual_tokens :, :] # Get predictions predictions = shift_logits.argmax(dim=-1) # Create mask for non-padding tokens (assuming ignore_index is -100) mask = shift_labels != -100 # Calculate accuracy only on non-padding tokens correct_predictions = (predictions == shift_labels) & mask total_tokens = mask.sum() correct_tokens = correct_predictions.sum() # Gather the correct_tokens and total_tokens across all processes correct_tokens = self.accelerator.gather_for_metrics(correct_tokens) total_tokens = self.accelerator.gather_for_metrics(total_tokens) # Compute the mean token accuracy and log it total_sum = total_tokens.sum() accuracy = (correct_tokens.sum() / total_sum).item() if total_sum > 0 else 0.0 self._metrics[mode]["mean_token_accuracy"].append(accuracy) # Log auxiliary loss if enabled (applies to both Liger and non-Liger) if self.aux_loss_enabled: aux_loss = outputs.aux_loss aux_loss = self.accelerator.gather_for_metrics(aux_loss).mean().item() self._metrics[mode]["aux_loss"].append(aux_loss) return (loss, outputs) if return_outputs else loss def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None): # Preserve the eval loop intent so compute_loss can decide whether logits are needed. inputs["_prediction_loss_only"] = prediction_loss_only return super().prediction_step(model, inputs, prediction_loss_only, ignore_keys=ignore_keys) # Override training step to add activation offloading context. def training_step(self, *args, **kwargs): with self.maybe_activation_offload_context: return super().training_step(*args, **kwargs) def log(self, logs: dict[str, float], start_time: float | None = None) -> None: mode = "train" if self.model.training else "eval" metrics = {key: sum(val) / len(val) for key, val in self._metrics[mode].items()} # average the metrics # This method can be called both in training and evaluation. When called in evaluation, the keys in `logs` # start with "eval_". We need to add the prefix "eval_" to the keys in `metrics` to match the format. if mode == "eval": metrics = {f"eval_{key}": val for key, val in metrics.items()} logs = {**logs, **metrics} super().log(logs, start_time) self._metrics[mode].clear() # Ensure the model card is saved along with the checkpoint def _save_checkpoint(self, model, trial): if self.args.hub_model_id is None: model_name = Path(self.args.output_dir).name else: model_name = self.args.hub_model_id.split("/")[-1] self.create_model_card(model_name=model_name) super()._save_checkpoint(model, trial) ================================================ FILE: trl/trainer/utils.py ================================================ # Copyright 2020-2026 The HuggingFace Team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import hashlib import importlib.resources as pkg_resources import os import random import socket import threading from collections.abc import Mapping, Sequence, Sized from contextlib import contextmanager from dataclasses import dataclass from importlib.metadata import version from itertools import accumulate from typing import TypeVar import numpy as np import pandas as pd import torch import torch.nn.functional as F import transformers from accelerate import PartialState, logging from accelerate.state import AcceleratorState from huggingface_hub import ModelCard, ModelCardData from torch.utils.data import Sampler from transformers import ( AutoConfig, BitsAndBytesConfig, PretrainedConfig, PreTrainedModel, is_comet_available, is_trackio_available, ) from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast from transformers.models.auto.auto_factory import _BaseAutoModelClass from transformers.utils import ( is_peft_available, is_rich_available, is_torch_xpu_available, ) from ..trainer.model_config import ModelConfig if is_rich_available(): from rich.console import Console from rich.panel import Panel from rich.table import Table from rich.text import Text if is_comet_available(): import comet_ml if is_peft_available(): from peft import LoraConfig, PeftConfig, PeftModel logger = logging.get_logger(__name__) def _is_port_free(port: int, host: str = "127.0.0.1") -> bool: try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((host, port)) return True except OSError: return False def _find_free_port() -> int: candidates = (29500, 23456, 12355, 12345) for p in candidates: if _is_port_free(p): return p with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) return s.getsockname()[1] def ensure_master_addr_port(addr: str | None = None, port: int | None = None) -> None: """ Ensure `MASTER_ADDR`/`MASTER_PORT` are set safely. - Respects existing environment variables. - Defaults `MASTER_ADDR` to localhost if unset. - Chooses a free TCP port if `MASTER_PORT` is unset to avoid collisions. - If `MASTER_PORT` is set to `"0"` or `"auto"`, it is resolved to a free port. """ os.environ["MASTER_ADDR"] = os.environ.get("MASTER_ADDR") or addr or "localhost" env_port = os.environ.get("MASTER_PORT", "").strip().lower() if port is None and env_port not in {"", "0", "auto"}: try: port = int(env_port) except ValueError: pass os.environ["MASTER_PORT"] = str(_find_free_port() if port in (None, 0) else port) def pad( tensors: list[torch.Tensor], padding_value: int = 0, padding_side: str = "right", pad_to_multiple_of: int | None = None, ) -> torch.Tensor: """ Pads a list of tensors to the same shape along the first dimension. Args: tensors (`list[torch.Tensor]`): List of input tensors to pad. padding_value (`int`): Value to use for padding. Default is 0. padding_side (`str`): Side on which to add padding. Must be 'left' or 'right'. Default is 'right'. pad_to_multiple_of (`int`, *optional*): If set will pad the sequence to a multiple of the provided value. Returns: `torch.Tensor`: A single tensor containing the padded tensors. Examples: ```python >>> import torch >>> pad([torch.tensor([1, 2, 3]), torch.tensor([4, 5])]) tensor([[1, 2, 3], [4, 5, 0]]) >>> pad([torch.tensor([[1, 2], [3, 4]]), torch.tensor([[5, 6]])]) tensor([[[1, 2], [3, 4]], [[5, 6], [0, 0]]]) ``` """ # Determine the maximum shape for each dimension output_shape = np.max([t.shape for t in tensors], 0).tolist() # Apply pad_to_multiple_of to the first (sequence) dimension if pad_to_multiple_of is not None: remainder = output_shape[0] % pad_to_multiple_of if remainder != 0: output_shape[0] += pad_to_multiple_of - remainder # Create an output tensor filled with the padding value output = torch.full((len(tensors), *output_shape), padding_value, dtype=tensors[0].dtype, device=tensors[0].device) for i, t in enumerate(tensors): if padding_side == "left": seq_start = output_shape[0] - t.shape[0] elif padding_side == "right": seq_start = 0 else: raise ValueError("padding_side must be 'left' or 'right'") # Define the slices seq_slice = slice(seq_start, seq_start + t.shape[0]) slices = (seq_slice,) + tuple(slice(0, s) for s in t.shape[1:]) output[i][slices] = t return output def disable_dropout_in_model(model: torch.nn.Module) -> None: for module in model.modules(): if isinstance(module, torch.nn.Dropout): module.p = 0 def get_quantization_config(model_args: ModelConfig) -> BitsAndBytesConfig | None: if model_args.load_in_4bit: quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=model_args.dtype, # For consistency with model weights, we use the same value as `dtype` bnb_4bit_quant_type=model_args.bnb_4bit_quant_type, bnb_4bit_use_double_quant=model_args.use_bnb_nested_quant, bnb_4bit_quant_storage=model_args.bnb_4bit_quant_storage, ) elif model_args.load_in_8bit: quantization_config = BitsAndBytesConfig( load_in_8bit=True, ) else: quantization_config = None return quantization_config def get_kbit_device_map() -> dict[str, int] | None: if torch.cuda.is_available() or is_torch_xpu_available(): return {"": PartialState().local_process_index} else: return None def get_peft_config(model_args: ModelConfig) -> "PeftConfig | None": if model_args.use_peft is False: return None if not is_peft_available(): raise ValueError( "You need to have PEFT library installed in your environment, make sure to install `peft`. " "Make sure to run `pip install -U peft`." ) peft_config = LoraConfig( task_type=model_args.lora_task_type, r=model_args.lora_r, target_modules=model_args.lora_target_modules, target_parameters=model_args.lora_target_parameters, lora_alpha=model_args.lora_alpha, lora_dropout=model_args.lora_dropout, bias="none", use_rslora=model_args.use_rslora, use_dora=model_args.use_dora, modules_to_save=model_args.lora_modules_to_save, ) return peft_config def prepare_deepspeed( model: torch.nn.Module, per_device_train_batch_size: int, fp16: bool = False, bf16: bool = False ) -> torch.nn.Module: """ Prepares the model for training with DeepSpeed (both for stage 2 and 3), configuring the appropriate settings based on the model and batch size. Args: model (`torch.nn.Module`): The model to be prepared for DeepSpeed training. per_device_train_batch_size (`int`): The training batch size per device. fp16 (`bool`, defaults to `False`): Whether to use FP16 precision. bf16 (`bool`, defaults to `False`): Whether to use BF16 precision. Returns: `torch.nn.Module`: The model initialized and configured with DeepSpeed for training. """ import deepspeed deepspeed_plugin = AcceleratorState().deepspeed_plugin config_kwargs = deepspeed_plugin.deepspeed_config if config_kwargs["zero_optimization"]["stage"] != 3: config_kwargs["train_micro_batch_size_per_gpu"] = per_device_train_batch_size config_kwargs = { "train_micro_batch_size_per_gpu": config_kwargs["train_micro_batch_size_per_gpu"], "prescale_gradients": False, "wall_clock_breakdown": False, } if bf16: config_kwargs["bf16"] = {"enabled": True} elif fp16: config_kwargs["fp16"] = {"enabled": True} else: if hasattr(model, "config"): hidden_size = ( max(model.config.hidden_sizes) if getattr(model.config, "hidden_sizes", None) else getattr(model.config, "hidden_size", None) ) if hidden_size is not None and config_kwargs["zero_optimization"]["stage"] == 3: # Note that `stage3_prefetch_bucket_size` can produce DeepSpeed messages like: `Invalidate trace cache @ step 0: expected module 1, but got module 0` # This is expected and is not an error, see: https://github.com/microsoft/DeepSpeed/discussions/4081 config_kwargs.update( { "zero_optimization.reduce_bucket_size": hidden_size * hidden_size, "zero_optimization.stage3_param_persistence_threshold": 10 * hidden_size, "zero_optimization.stage3_prefetch_bucket_size": 0, } ) model, *_ = deepspeed.initialize(model=model, config=config_kwargs) model.eval() return model def generate_model_card( base_model: str | None, model_name: str, hub_model_id: str, dataset_name: str | None, tags: list[str], wandb_url: str | None, trackio_url: str | None, trainer_name: str, trainer_citation: str | None = None, template_file: str | None = None, paper_title: str | None = None, paper_id: str | None = None, comet_url: str | None = None, ) -> ModelCard: """ Generate a [`~huggingface_hub.ModelCard`] from a template. Args: base_model (`str` or `None`): Base model name. model_name (`str`): Model name. hub_model_id (`str`): Hub model ID as `username/model_id`. dataset_name (`str` or `None`): Dataset name. tags (`list[str]`): Tags. wandb_url (`str` or `None`): Weights & Biases run URL. trackio_url (`str` or `None`): Trackio Space URL. comet_url (`str` or `None`): Comet experiment URL. trainer_name (`str`): Trainer name. trainer_citation (`str` or `None`, defaults to `None`): Trainer citation as a BibTeX entry. template_file (`str` *optional*): Template file name located in the `trl/templates` directory. Defaults to `lm_model_card.md`. paper_title (`str` or `None`, defaults to `None`): Paper title. paper_id (`str` or `None`, defaults to `None`): ArXiv paper ID as `YYMM.NNNNN`. Returns: [`~huggingface_hub.ModelCard`]: A ModelCard object. """ card_data = ModelCardData( base_model=base_model, datasets=dataset_name, library_name="transformers", licence="license", model_name=model_name, tags=["generated_from_trainer", *tags], ) template_file = template_file or "lm_model_card.md" card = ModelCard.from_template( card_data, template_path=str(pkg_resources.files("trl").joinpath(f"templates/{template_file}")), base_model=base_model, model_name=model_name, hub_model_id=hub_model_id, dataset_name=dataset_name, wandb_url=wandb_url, trackio_url=trackio_url, comet_url=comet_url, trainer_name=trainer_name, trainer_citation=trainer_citation, paper_title=paper_title, paper_id=paper_id, trl_version=version("trl"), transformers_version=version("transformers"), pytorch_version=version("torch"), datasets_version=version("datasets"), tokenizers_version=version("tokenizers"), ) return card def get_comet_experiment_url() -> str | None: """ If Comet integration is enabled, return the URL of the current Comet experiment; otherwise, return `None`. """ if not is_comet_available(): return None if comet_ml.get_running_experiment() is not None: return comet_ml.get_running_experiment().url return None def get_trackio_space_url() -> str | None: """ If Trackio integration is enabled, return the URL of the current Trackio Space; otherwise, return `None`. """ if not is_trackio_available(): return None from trackio import context_vars run = context_vars.current_run.get() if run is None: return None space_id = run._space_id if space_id is None: return None space_id = space_id.replace("/", "-") project = run.project name = run.name return f"https://{space_id}.hf.space?project={project}&runs={name}&sidebar=collapsed" def log_table_to_comet_experiment(name: str, table: pd.DataFrame) -> None: """ If Comet integration is enabled logs a table to the Comet experiment if it is currently running. Args: name (`str`): Table name. table (`pandas.DataFrame`): The Pandas DataFrame containing the table to log. """ if not is_comet_available(): raise ModuleNotFoundError("The comet-ml is not installed. Please install it first: pip install comet-ml") experiment = comet_ml.get_running_experiment() if experiment is not None: experiment.log_table(tabular_data=table, filename=name) def flush_left(mask: torch.Tensor, *tensors: torch.Tensor) -> torch.Tensor | tuple[torch.Tensor, ...]: """ Shift non-zero elements in the mask and corresponding tensors to the left. This function operates on a binary mask and any number of additional tensors with the same dimensions as the mask. For each row, non-zero values are shifted to the leftmost positions. Then, columns that contain only zeros across all rows are truncated from the mask and tensors. Visually, this operation can be represented as follows: ``` [[0, 0, x, x, x, x], -> [[x, x, x, x], [0, x, x, x, 0, 0]] [x, x, x, 0]] ``` Args: mask (`torch.Tensor`): 2D tensor (binary mask) with shape `(N, M)`. *tensors (`torch.Tensor`): One or more 2D tensors with the same shape as `mask`. These tensors will be processed alongside `mask`, with non-zero values shifted and excess zero columns truncated in the same manner. Returns: `torch.Tensor`: Updated binary mask with non-zero values flushed to the left and trailing zero columns removed. `*torch.Tensor` Updated tensors, processed in the same way as the mask. Example: ```python >>> mask = torch.tensor([[0, 0, 1, 1, 1], [0, 1, 1, 0, 0]]) >>> tensor = torch.tensor([[9, 9, 2, 3, 4], [9, 5, 6, 9, 9]]) >>> new_mask, new_tensor = flush_left(mask, tensor) >>> print(new_mask) tensor([[1, 1, 1], [1, 1, 0]]) >>> print(new_tensor) tensor([[2, 3, 4], [5, 6, 0]]) ``` """ _, M = mask.shape # Create copy of mask and tensors mask_copy = mask.clone() tensors = [t.clone() for t in tensors] # Shift non-zero values to the left first_non_zero = mask_copy.argmax(dim=1) pos = torch.arange(M, device=mask_copy.device).unsqueeze(0) idx_roll = (pos + first_non_zero.unsqueeze(1)) % M mask_roll = mask_copy.gather(1, idx_roll) rolled_tensors = [t.gather(1, idx_roll) for t in tensors] # Truncate trailing columns that are all zeros in mask_roll col_sums = mask_roll.sum(dim=0) empty_cols = col_sums == 0 first_empty_col = int(empty_cols.to(torch.int8).argmax()) if empty_cols.any() else M flushed_mask = mask_roll[:, :first_empty_col] flushed_tensors = [t[:, :first_empty_col] for t in rolled_tensors] if not flushed_tensors: return flushed_mask return flushed_mask, *flushed_tensors def flush_right(mask: torch.Tensor, *tensors: torch.Tensor) -> torch.Tensor | tuple[torch.Tensor, ...]: """ Shift non-zero elements in the mask and corresponding tensors to the right. See `flush_left` for details. """ _, M = mask.shape # Create copy of mask and tensors mask_copy = mask.clone() tensors = [t.clone() for t in tensors] # Shift non-zero values to the right flipped_mask = torch.fliplr(mask_copy) first_non_zero = flipped_mask.argmax(dim=1) pos = torch.arange(M, device=mask_copy.device).unsqueeze(0) idx_roll = (pos - first_non_zero.unsqueeze(1)) % M mask_roll = mask_copy.gather(1, idx_roll) rolled_tensors = [t.gather(1, idx_roll) for t in tensors] # Truncate leading columns that are all zeros in mask_roll col_sums = mask_roll.sum(dim=0) non_empty_cols = col_sums != 0 first_non_empty_col = int(non_empty_cols.to(torch.int8).argmax()) if non_empty_cols.any() else M flushed_mask = mask_roll[:, first_non_empty_col:] flushed_tensors = [t[:, first_non_empty_col:] for t in rolled_tensors] if not flushed_tensors: return flushed_mask return flushed_mask, *flushed_tensors def selective_log_softmax(logits, index) -> torch.Tensor: """ A memory-efficient implementation of the common `log_softmax -> gather` operation. This function is equivalent to the following naive implementation: ```python # for index with shape (...): logps = torch.gather(logits.log_softmax(-1), dim=-1, index=index.unsqueeze(-1)).squeeze(-1) # for index with shape (..., K): logps = torch.gather(logits.log_softmax(-1), dim=-1, index=index) ``` Args: logits (`torch.Tensor`): Logits tensor of shape `(..., num_classes)`. index (`torch.Tensor`): Index tensor of shape `(..., K)` or `(...)`, specifying the positions to gather from the log-softmax output. When the last case is used, `K` log-probabilities are gathered per position (e.g. for top-K) Returns: `torch.Tensor`: Gathered log probabilities with the same shape as `index`. """ squeeze = index.ndim == logits.ndim - 1 if squeeze: index = index.unsqueeze(-1) if logits.dtype in [torch.float32, torch.float64]: selected_logits = torch.gather(logits, dim=-1, index=index) # loop to reduce peak mem consumption logsumexp_values = torch.stack([torch.logsumexp(lg, dim=-1) for lg in logits]) per_token_logps = selected_logits - logsumexp_values.unsqueeze(-1) # log_softmax(x_i) = x_i - logsumexp(x) else: # logsumexp approach is unstable with bfloat16, fall back to slightly less efficient approach per_token_logps = [] for row_logits, row_labels in zip(logits, index, strict=True): # loop to reduce peak mem consumption row_logps = F.log_softmax(row_logits, dim=-1) row_per_token_logps = row_logps.gather(dim=-1, index=row_labels) per_token_logps.append(row_per_token_logps) per_token_logps = torch.stack(per_token_logps) if squeeze: per_token_logps = per_token_logps.squeeze(-1) return per_token_logps def entropy_from_logits(logits: torch.Tensor, chunk_size: int = 128) -> torch.Tensor: """ Compute the Shannon entropy (in nats) for each row of *logits* in a memory-efficient way. Instead of materializing the full softmax for all rows at once, the logits are flattened to shape (N, num_classes), where N is the product of all leading dimensions. Computation is then performed in chunks of size `chunk_size` along this flattened dimension, reducing peak memory usage. The result is reshaped back to match the input's leading dimensions. Args: logits (`torch.Tensor`): Logits tensor of shape `(..., num_classes)`. Entropy is taken along the last axis; all leading dimensions are preserved in the output. chunk_size (`int`, *optional*, defaults to `128`): Number of rows from the flattened logits to process per iteration. Smaller values reduce memory usage at the cost of more iterations. Returns: `torch.Tensor`: Entropy values with shape `logits.shape[:-1]`. """ original_shape = logits.shape[:-1] # all dims except num_classes num_classes = logits.shape[-1] # Flatten all leading dimensions into one flat_logits = logits.reshape(-1, num_classes) entropies = [] for chunk in flat_logits.split(chunk_size, dim=0): logps = F.log_softmax(chunk, dim=-1) chunk_entropy = -(torch.exp(logps) * logps).sum(-1) entropies.append(chunk_entropy) entropies = torch.cat(entropies, dim=0) return entropies.reshape(original_shape) def print_prompt_completions_sample( prompts: list, completions: list, rewards: dict[str, list[float]], advantages: list[float], step: int, num_samples: int = None, ) -> None: """ Print out a sample of model completions to the console with multiple reward metrics. This function creates a nicely formatted table showing prompt-completion pairs, useful for monitoring model outputs during training. It requires the `rich` library to be installed. Args: prompts (`list`): List of prompts. Can be either strings or lists of messages. completions (`list`): List of completions corresponding to the prompts. Can be either strings or lists of messages. rewards (`dict[str, list[float]]`): Dictionary where keys are reward names and values are lists of rewards. advantages (`list[float]`): List of advantages corresponding to the prompts and completions. step (`int`): Current training step number, used in the output title. num_samples (`int`, *optional*): Number of random samples to display. If `None` (default), all items will be displayed. Example: ```python >>> from trl.trainer.utils import print_prompt_completions_sample >>> prompts = ["The sky is", "The sun is"] >>> completions = [" blue.", " in the sky."] >>> rewards = {"Correctness": [0.123, 0.456], "Format": [0.789, 0.101]} >>> advantages = [0.987, 0.654] >>> print_prompt_completions_sample(prompts, completions, rewards, advantages, 42) ╭──────────────────────────── Step 42 ─────────────────────────────╮ │ ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━┓ │ │ ┃ Prompt ┃ Completion ┃ Correctness ┃ Format ┃ Advantage ┃ │ │ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━┩ │ │ │ The sky is │ blue. │ 0.12 │ 0.79 │ 0.99 │ │ │ ├────────────┼──────────────┼─────────────┼────────┼───────────┤ │ │ │ The sun is │ in the sky. │ 0.46 │ 0.10 │ 0.65 │ │ │ └────────────┴──────────────┴─────────────┴────────┴───────────┘ │ ╰──────────────────────────────────────────────────────────────────╯ ``` """ if not is_rich_available(): raise ImportError( "The function `print_prompt_completions_sample` requires the `rich` library. Please install it with " "`pip install rich`." ) console = Console() table = Table(show_header=True, header_style="bold white", expand=True) # Add columns table.add_column("Prompt", style="bright_yellow") table.add_column("Completion", style="bright_green") for reward_name in rewards.keys(): table.add_column(reward_name, style="bold cyan", justify="right") table.add_column("Advantage", style="bold magenta", justify="right") def format_entry(entry) -> Text: t = Text() if isinstance(entry, list) and all(isinstance(m, dict) for m in entry): for j, msg in enumerate(entry): role = msg.get("role", "") if "content" in msg: # Chat message t.append(f"{role.upper()}\n", style="bold red") t.append(msg["content"]) elif "name" in msg and "args" in msg: # Tool call t.append(f"{role.upper()}\n", style="bold red") t.append(f"{msg['name']}({msg['args']})") else: # Fallback t.append(str(msg)) if j < len(entry) - 1: t.append("\n\n") else: t.append(str(entry)) return t # Some basic input validation if num_samples is not None: if num_samples >= len(prompts): num_samples = None elif num_samples <= 0: return # Subsample data if num_samples is specified if num_samples is not None: indices = random.sample(range(len(prompts)), num_samples) prompts = [prompts[i] for i in indices] completions = [completions[i] for i in indices] rewards = {key: [val[i] for i in indices] for key, val in rewards.items()} advantages = [advantages[i] for i in indices] for i in range(len(prompts)): reward_values = [f"{rewards[key][i]:.2f}" for key in rewards.keys()] # 2 decimals table.add_row( format_entry(prompts[i]), format_entry(completions[i]), *reward_values, f"{advantages[i]:.2f}", ) table.add_section() # Adds a separator between rows panel = Panel(table, expand=False, title=f"Step {step}", border_style="bold white") console.print(panel) class RepeatSampler(Sampler): """ Sampler that repeats the indices of a dataset in a structured manner. Args: data_source (`Sized`): Dataset to sample from. mini_repeat_count (`int`): Number of times to repeat each index per batch. batch_size (`int`, *optional*, defaults to `1`): Number of unique indices per batch. repeat_count (`int`, *optional*, defaults to `1`): Number of times to repeat the full sampling process. shuffle (`bool`, *optional*, defaults to `True`): Whether to shuffle the dataset. seed (`int`, *optional*): Random seed for reproducibility (only affects this sampler). Example: ```python >>> sampler = RepeatSampler(["a", "b", "c", "d", "e", "f", "g"], mini_repeat_count=2, batch_size=3, repeat_count=4) >>> list(sampler) [4, 4, 3, 3, 0, 0, 4, 4, 3, 3, 0, 0, 4, 4, 3, 3, 0, 0, 4, 4, 3, 3, 0, 0, 1, 1, 2, 2, 6, 6, 1, 1, 2, 2, 6, 6, 1, 1, 2, 2, 6, 6, 1, 1, 2, 2, 6, 6] ``` ```txt mini_repeat_count = 3 - - - [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, | 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, | 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, | repeat_count = 2 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, | 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, | 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, ...] | --------- --------- --------- --------- --------- --------- --------- --------- --------- --------- --------- --------- batch_size = 12 ``` """ def __init__( self, data_source: Sized, mini_repeat_count: int, batch_size: int = 1, repeat_count: int = 1, shuffle: bool = True, seed: int | None = None, ): self.data_source = data_source self.mini_repeat_count = mini_repeat_count self.batch_size = batch_size self.repeat_count = repeat_count self.num_samples = len(data_source) self.shuffle = shuffle self.seed = seed if shuffle: self.generator = torch.Generator() # Create a local random generator if seed is not None: self.generator.manual_seed(seed) def __iter__(self): if self.shuffle: # E.g., [2, 4, 3, 1, 0, 6, 5] (num_samples = 7) indexes = torch.randperm(self.num_samples, generator=self.generator).tolist() else: indexes = list(range(self.num_samples)) # [2, 4, 3, 1, 0, 6, 5] # -> [[2, 4, 3], [1, 0, 6], [5]] (batch_size = 3) indexes = [indexes[i : i + self.batch_size] for i in range(0, len(indexes), self.batch_size)] # [[2, 4, 3], [1, 0, 6], [5]] # -> [[2, 4, 3], [1, 0, 6]] indexes = [chunk for chunk in indexes if len(chunk) == self.batch_size] for chunk in indexes: for _ in range(self.repeat_count): for index in chunk: for _ in range(self.mini_repeat_count): yield index def __len__(self) -> int: return (self.num_samples // self.batch_size) * self.batch_size * self.mini_repeat_count * self.repeat_count # torch.nanstd doesn't exist, so we define it here def nanstd(tensor: torch.Tensor, dim: int | tuple[int, ...] | None = None, keepdim: bool = False) -> torch.Tensor: """ Compute the standard deviation of a tensor, ignoring NaNs. Args: tensor (`torch.Tensor`): Input tensor. dim (`int` or `tuple[int, ...]`, *optional*): Dimension(s) to reduce. Defaults to all dimensions. keepdim (`bool`, *optional*, defaults to `False`): Whether to keep reduced dimensions. Returns: `torch.Tensor`: Standard deviation of the tensor, ignoring NaNs. """ # Compute variance ignoring NaNs mean = torch.nanmean(tensor, dim=dim, keepdim=True) variance = torch.nanmean((tensor - mean) ** 2, dim=dim, keepdim=True) count = torch.sum(~torch.isnan(tensor), dim=dim, keepdim=True) # count of non-NaN values correction = count / (count - 1) correction = torch.where(count > 1, correction, torch.full_like(correction, float("nan"))) variance *= correction # Bessel's correction std = torch.sqrt(variance) if keepdim: return std if dim is None: return std.squeeze() if isinstance(dim, int): return std.squeeze(dim) dims = [(d if d >= 0 else d + std.ndim) for d in dim] for d in sorted(dims, reverse=True): std = std.squeeze(d) return std def split_tensor_dict( tensor_dict: dict[str, torch.Tensor | None], num_chunks: int ) -> list[dict[str, torch.Tensor | None]]: """ Splits a dictionary of tensors along the first dimension into `num_chunks` equal parts. Example: ```python >>> x = torch.arange(12).reshape(6, 2) >>> y = torch.arange(6).reshape(6, 1) >>> tensor_dict = {"x": x, "y": y} >>> split_tensor_dict(tensor_dict, 3) [ {"x": tensor([[0, 1], [2, 3]]), "y": tensor([[0], [1]])}, {"x": tensor([[4, 5], [6, 7]]), "y": tensor([[2], [3]])}, {"x": tensor([[ 8, 9], [10, 11]]), "y": tensor([[4], [5]])} ] ``` """ first_tensor = next(tensor for tensor in tensor_dict.values() if tensor is not None) chunk_size = first_tensor.shape[0] // num_chunks chunks = [] for i in range(num_chunks): chunk_dict = {} for key, tensor in tensor_dict.items(): if tensor is not None and (isinstance(tensor, list) or tensor.ndim > 0): chunk_dict[key] = tensor[i * chunk_size : (i + 1) * chunk_size] elif tensor is not None and tensor.ndim == 0: chunk_dict[key] = tensor else: chunk_dict[key] = None chunks.append(chunk_dict) return chunks def shuffle_sequence_dict(seq_dict: dict[str, Sequence | None]) -> dict[str, Sequence | None]: """ Shuffles all sequence-like values in a dictionary along the first dimension in unison. Example: ```python >>> x = torch.arange(6).reshape(3, 2) >>> y = ["a", "b", "c"] >>> seq_dict = {"x": x, "y": y} >>> shuffle_sequence_dict(seq_dict) {'x': tensor([[2, 3], [0, 1], [4, 5]]), 'y': ['b', 'a', 'c']} ``` """ # Determine batch size from the first non-None sequence batch_size = len(next(v for v in seq_dict.values() if v is not None)) permutation = torch.randperm(batch_size) def permute(v: Sequence | None) -> Sequence | None: if v is None: return None if isinstance(v, torch.Tensor) and v.ndim == 0: return v if isinstance(v, torch.Tensor) and v.ndim >= 1: return v[permutation] return [v[i] for i in permutation] return {key: permute(val) for key, val in seq_dict.items()} def nanmin(tensor: torch.Tensor) -> torch.Tensor: """ Compute the minimum value of a tensor, ignoring NaNs. This function only supports 1D tensors. Args: tensor (`torch.Tensor`): Input tensor of shape `(N,)`. Returns: `torch.Tensor`: Minimum value of the tensor, ignoring NaNs. Returns NaN if all values are NaN. """ if torch.isnan(tensor).all(): return torch.tensor(float("nan"), dtype=tensor.dtype, device=tensor.device) return torch.min(tensor[~torch.isnan(tensor)]) def nanmax(tensor: torch.Tensor) -> torch.Tensor: """ Compute the maximum value of a tensor, ignoring NaNs. This function only supports 1D tensors. Args: tensor (`torch.Tensor`): Input tensor of shape `(N,)`. Returns: `torch.Tensor`: Maximum value of the tensor, ignoring NaNs. Returns NaN if all values are NaN. """ if torch.isnan(tensor).all(): return torch.tensor(float("nan"), dtype=tensor.dtype, device=tensor.device) return torch.max(tensor[~torch.isnan(tensor)]) def identity(x): """Do we really need docs for this?""" return x def split_pixel_values_by_grid(batch: dict[str, torch.Tensor]) -> dict[str, torch.Tensor | list[torch.Tensor]]: """ Splits `batch["pixel_values"]` into a list of tensors based on the product of each row in `batch["image_grid_thw"]` and batch["num_images"] while keeping other entries unchanged. """ if "image_grid_thw" not in batch or "pixel_values" not in batch or "num_images" not in batch: return batch lengths = batch["image_grid_thw"].prod(-1).tolist() # [num_images] pixel_values = batch["pixel_values"] # [total, feature_dim] if sum(lengths) != pixel_values.size(0): raise ValueError(f"Mismatch: sum(lengths) = {sum(lengths)} != pixel_values.size(0) = {pixel_values.size(0)}") boundaries = [0, *accumulate(batch["num_images"])] # [3, 4, 5] -> [0, 3, 7, 12] sections = [sum(lengths[boundaries[i] : boundaries[i + 1]]) for i in range(len(batch["num_images"]))] split_values = list(torch.split(batch["pixel_values"], sections, dim=0)) image_grid_thw = list(torch.split(batch["image_grid_thw"], batch["num_images"], dim=0)) return {**batch, "pixel_values": split_values, "image_grid_thw": image_grid_thw} def unsplit_pixel_values_by_grid(batch: dict[str, torch.Tensor | list[torch.Tensor]]) -> dict[str, torch.Tensor]: """ Opposite of `split_pixel_values_by_grid`. Merges a list of tensors in `batch["pixel_values"]` back into a single tensor along the first dimension. """ pixel_values = batch.get("pixel_values") if isinstance(pixel_values, list): merged = torch.cat(pixel_values, dim=0) batch = {**batch, "pixel_values": merged} image_grid_thw = batch.get("image_grid_thw") if isinstance(image_grid_thw, list): merged = torch.cat(image_grid_thw, dim=0) batch = {**batch, "image_grid_thw": merged} return batch TListOrMapping = TypeVar("TListOrMapping", list, Mapping) def remove_none_values(example: TListOrMapping) -> TListOrMapping: """ Recursively removes entries with `None` values from a nested structure (list or dictionary). Args: example (`list` or `Mapping`): Input nested structure (list or dictionary) from which to remove `None`. Example: ```python >>> [ ... { ... "a": {"aa": None, "ab": 1}, ... "b": "my_string", ... } ... ] >>> remove_none_values(example) [{'a': {'ab': 1}, 'b': 'my_string'}] ``` """ if isinstance(example, list): return [remove_none_values(value) if isinstance(value, (dict, list)) else value for value in example] elif isinstance(example, Mapping): return { key: remove_none_values(value) if isinstance(value, (dict, list)) else value for key, value in example.items() if value is not None } else: raise TypeError("Input must be a list or a dictionary.") def create_model_from_path( model_id: str, architecture: _BaseAutoModelClass | None = None, **kwargs ) -> PreTrainedModel: """ Create a model from a given path using the specified initialization arguments. Args: model_id (`str`): Path to the model. Can be either a local directory or a model identifier from the Hugging Face Hub. architecture (`_BaseAutoModelClass` or `None`, *optional*): Model architecture class to instantiate. The model is initialized using the `from_pretrained` method of this class. If `None`, the architecture will be inferred from the model's configuration. kwargs (`dict`): Initialization keyword arguments to pass to the model's `from_pretrained` method. When `'dtype'` is specified, it can be either a `torch.dtype` or one of the strings: `'bfloat16'`, `'float16'`, `'float32'`, or `'auto'`. If not explicitly set, `dtype` defaults to `'float32'`. Returns: [`~transformers.PreTrainedModel`]: The instantiated model. """ dtype = kwargs.get("dtype", "float32") if isinstance(dtype, torch.dtype) or dtype == "auto" or dtype is None: pass # dtype is already a torch.dtype or "auto" or None elif isinstance(dtype, str) and dtype in ["bfloat16", "float16", "float32"]: kwargs["dtype"] = getattr(torch, dtype) else: raise ValueError( "Invalid `dtype` passed to the config. Expected either 'auto' or a string representing " f"a valid `torch.dtype` (e.g., 'float32'), but got {dtype}." ) kwargs["device_map"] = kwargs.get("device_map", "auto") if architecture is None: config = AutoConfig.from_pretrained(model_id) architecture = getattr(transformers, config.architectures[0]) model = architecture.from_pretrained(model_id, **kwargs) return model def hash_module(module: torch.nn.Module) -> str: h = hashlib.sha256() for _, tensor in sorted(module.state_dict().items()): tensor = tensor.cpu() h.update(str(tensor.dtype).encode()) if tensor.dtype in [torch.bfloat16, torch.float8_e4m3fn, torch.float8_e5m2]: tensor = tensor.to(torch.float32) h.update(tensor.numpy().tobytes()) return h.hexdigest() def get_config_model_id(config: PretrainedConfig) -> str: """ Retrieve the model identifier from a given model configuration. Args: config ([`~transformers.PreTrainedConfig`]): Configuration from which to extract the model identifier. Returns: `str`: The model identifier associated with the model configuration. """ return getattr(config, "_name_or_path", "") @dataclass class CausalLMOutputWithPastAndFlatLogits(CausalLMOutputWithPast): flat_logits: torch.Tensor | None = None def forward_masked_logits( model: PreTrainedModel, logits_mask: torch.LongTensor, **kwargs ) -> CausalLMOutputWithPastAndFlatLogits: """ Run a Causal LM forward pass while computing logits only for masked positions to reduce memory usage. These are always equal: ```python full_outputs = model(input_ids=input_ids) masked_outputs = forward_masked_logits(model, mask, input_ids=input_ids) assert torch.equal( masked_outputs.flat_logits, full_outputs.logits[mask.bool()], ) ``` Args: model ([`~transformers.PreTrainedModel`]): A causal language model. logits_mask (`torch.LongTensor`): Boolean-like tensor indicating which token positions should have logits computed. Shape should match the input sequence shape in `kwargs` (typically `[batch, seq_len]`). **kwargs: Keyword arguments forwarded to the inner decoder (e.g., `input_ids`, `attention_mask`, `past_key_values`). Returns: `CausalLMOutputWithPastAndFlatLogits`: Output containing logits only for the unmasked positions. Raises: ValueError: If `logits_to_keep` or `labels` are provided in `kwargs`. """ if kwargs.get("logits_to_keep") is not None: raise ValueError("`logits_to_keep` is not supported by this forward helper.") if kwargs.get("labels") is not None: raise ValueError("`labels` is not yet supported by this forward helper.") outputs: BaseModelOutputWithPast = model.get_decoder()(**kwargs) hidden_states = outputs.last_hidden_state # Only compute necessary logits, and do not upcast them to float if we are not computing the loss flat_logits = model.lm_head(hidden_states[logits_mask.bool()]) if hasattr(model, "logit_scale"): # CohereForCausalLM has this attribute flat_logits = flat_logits * model.logit_scale return CausalLMOutputWithPastAndFlatLogits( flat_logits=flat_logits, # We use .get(...) because some models like FalconMambaForCausalLM don't return past_key_values or attentions past_key_values=outputs.get("past_key_values"), hidden_states=outputs.hidden_states, attentions=outputs.get("attentions"), ) @contextmanager def use_adapter(model: "PeftModel", adapter_name: str | None): """ Context manager to temporarily set and reset the active adapter in a PEFT model. Args: model ([`~peft.PeftModel`]): PEFT model to manage. adapter_name (`str` or `None`): Name of the adapter to set as active. If `None`, the context manager will disable all adapters. Example: ```python >>> from trl.trainer.utils import use_adapter >>> from peft import AutoPeftModelForCausalLM >>> import torch >>> model = AutoPeftModelForCausalLM.from_pretrained("path/to/model") >>> input_ids = torch.tensor([[1, 2, 3]]) >>> with use_adapter(model, "adapter_name"): ... outputs = model(input_ids) ``` """ if not is_peft_available(): raise ImportError( "You're trying to use a PEFT adapter but PEFT is not installed. Please install it with `pip install peft`." ) if adapter_name is None: with model.disable_adapter(): yield else: previous_adapter = model.active_adapter model.set_adapter(adapter_name) try: yield finally: model.set_adapter(previous_adapter) def start_event_loop_in_daemon( name: str | None = None, ) -> tuple[threading.Thread, asyncio.AbstractEventLoop, threading.Event]: """ This function creates a new daemon thread that runs the provided event loop. Args: name (`str`, *optional*): Name of the thread. If `None`, the default thread naming will be used. Returns: `threading.Thread`: The thread running the event loop. `asyncio.AbstractEventLoop`: The event loop being run in the thread. `threading.Event`: An event that is set when the loop is ready. """ loop = asyncio.new_event_loop() loop_ready_event = threading.Event() def run_loop(): asyncio.set_event_loop(loop) loop_ready_event.set() loop.run_forever() thread = threading.Thread(target=run_loop, name=name, daemon=True) thread.start() return thread, loop, loop_ready_event def shutdown_event_loop_in_daemon( thread: threading.Thread | None, loop: asyncio.AbstractEventLoop | None, ) -> None: """ Shutdown an asyncio event loop running in a separate thread. This function stops the event loop and waits for the associated thread to finish execution. Args: thread (`threading.Thread`): The thread running the event loop. loop (`asyncio.AbstractEventLoop`): The asyncio event loop to shut down. """ if loop is None or thread is None: return loop.call_soon_threadsafe(loop.stop) thread.join(timeout=5)