# 内存高效型 RL

我们很高兴在 Unsloth 中引入更高效的强化学习 (RL)，包含多项算法改进：

* **上下文长度提高 1.2 到 1.7 倍** 无任何速度变慢且无需额外内存！
* **强化学习训练运行加速 10%** 通过重构内核和异步数据移动实现
* **快 2 倍 `torch.compile` 次** 在模型加载期间

Unsloth **已经** 相较于所有其他使用 FA2 的配置，提升了 RL 训练速度、上下文窗口并将 VRAM 使用减少 50–90%，但现在 [**Unsloth 的待机（Standby）**](#unsloth-standby) 进一步改进了这一点。我们的 Standby 功能在限制速度下降方面独具优势，有时还能使训练更快！

现在，Qwen3-32B LoRA 16 位在单个 H100 80GB GPU 上可以达到 6,144 的上下文长度，而此前为 3,600（**长 1.7 倍**)。Llama-3.1-8B QLoRA 4 位可以达到 47,500 长度，而此前为 42,000（长 1.13 倍）。

我们通过各种内核优化使 RL 运行加速 10%，并在从训练切换到推理模式时移除了 CPU 与 GPU 之间的 LoRA 通信通道。最后，我们使用自定义 `torch.compile` 标志使 vLLM 的 rollout 加速 10%，并将编译时间减少 2 倍。

## :sparkles:如何启用优化

要启用 **Unsloth 的待机（Standby）** 功能，请在任何 Unsloth 导入之前设置环境变量 `UNSLOTH_VLLM_STANDBY` 然后设置 `gpu_memory_utilization = 0.95` 就这样！

```python
import os
os.environ["UNSLOTH_VLLM_STANDBY"] = "1"

from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-8B-Base",
    max_seq_length = 2048, # 对于更长的推理轨迹可以增加
    load_in_4bit = False, # LoRA 16 位时为 False
    fast_inference = True,
    max_lora_rank = 32, # 更大的秩 = 更聪明，但更慢
    gpu_memory_utilization = 0.95,
)
```

## :mortar\_board:不再需要 `gpu_memory_utilization`!

有了 Unsloth 的新 RL 改进，您再也不必担心调整或设置 `gpu_memory_utilization` 了——只需将其设置为 90% 或 95% 的 GPU 利用率即可——可惜 100% 无法使用，因为仍需为小张量保留一些空间。此前需要将其从 30% 调整到 95%——现在不必了！设到最大值，Unsloth 会处理剩下的！

## :interrobang:为什么 RL 要使用这么多内存？

GRPO（以及许多 RL 变体）在很大程度上依赖生成，而生成主要由 vLLM 驱动。但这代价很高，因为它需要持续的 **GPU 内存用于权重、激活和 KV 缓存**.

{% columns %}
{% column %}
推理占用大量 VRAM

<figure><img src="/files/81a2f01d7947ed2ab37f21772b23f90c81c968d9" alt=""><figcaption></figcaption></figure>
{% endcolumn %}

{% column %}
训练也会使用 VRAM！

<figure><img src="/files/ab631793dee90c40578ddccedfa275a45d59f702" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

这意味着 RL 需要在 GPU 上同时保留两套 VRAM/内存：

1. 推理引擎（包含模型权重、KV 缓存）
2. 训练引擎（包含模型权重、激活、梯度、优化器状态）

当前的 RL 框架不得不在 80GB GPU 上 50/50 划分，50% 给推理，50% 给训练。从训练模式切换到推理模式时移动权重可能需要相当长的时间。

<table><thead><tr><th width="251.51666259765625">80GB GPU</th><th>推理引擎（50%）</th><th>训练引擎（50%）</th></tr></thead><tbody><tr><td>模型权重</td><td>16GB</td><td>16GB</td></tr><tr><td>KV 缓存</td><td>24GB</td><td></td></tr><tr><td>激活、梯度、优化器状态</td><td></td><td>24GB</td></tr></tbody></table>

之前的 Unsloth 版本已经智能地优化了上述内容，因为我们 **直接共享 vLLM 的权重空间，这消除了模型权重的双重内存使用**。例如，这释放了 16GB 的空间，可用于增加上下文长度或提高生成速度。此外，我们不需要进行内存移动，这也使训练更快。

| 80GB GPU    | 推理引擎（50%）             | 训练引擎（50%）            |
| ----------- | --------------------- | -------------------- |
| 模型权重        | **16GB 共享**           | **<<< 共享**           |
| KV 缓存       | 24GB + 8GB = **32GB** |                      |
| 激活、梯度、优化器状态 |                       | 24GB + 8GB =**32GB** |

## 🦥Unsloth 待机（Standby）

但我们可以走得更远——我们首先注意到 RL 会交替进行推理然后训练然后再推理再训练等。

<figure><img src="/files/d94da3bcb5b5683f51013162f63da10f6886328f" alt=""><figcaption></figcaption></figure>

这意味着理论上推理和训练的内存空间可以被重复使用，因为推理和训练是不同的模式——这就是 [vLLM 的睡眠（sleep）模式功能](https://docs.vllm.ai/en/latest/features/sleep_mode.html#rlhf-weight-updates) 的作用，它有两个选项：

1. `level = 1` 将权重复制到 CPU 并删除 KV 缓存
2. `level = 2` 删除权重并删除 KV 缓存

但请注意在 Unsloth 中我们共享 vLLM 的权重内存空间——这意味着我们需要一种新的方法来删除 KV 缓存并忽略删除权重，我们称之为 Unsloth Standby。

| 80GB GPU                                                                                                                                               | 推理引擎        | 训练引擎        |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ----------- |
| 模型权重                                                                                                                                                   | **16GB 共享** | **<<< 共享**  |
| <p><mark style="background-color:purple;"><strong>多用途</strong></mark></p><p><mark style="background-color:purple;"><strong>64GB 空间</strong></mark></p> | KV 缓存       | 激活、梯度、优化器状态 |

要启用此功能，只需在任何 Unsloth 导入之前将以下内容添加到所有 RL / GRPO 训练运行中：

```python
import os
os.environ["UNSLOTH_VLLM_STANDBY"] = "1"
```

## 🧪性能实验

在这里您将看到我们如何为 GRPO 基准测试内存使用和上下文长度。请注意我们进行 **每个提示 2 次生成，因为为了使 GRPO 生效**，我们需要至少 2 次生成以计算样本均值和方差。 **如果没有 2 次生成，单样本的标准差为 0**。这会导致使用该值的优势 (reward - mean)/std **变得未定义**.

$$
Z=\frac{r\_i - \mu}{\sqrt{\frac{1}{n}\sum(r\_i-\mu)^2}} \\
Z\_{n=1}=\frac{r\_1 - \mu}{\sqrt{\frac{1}{1}\sum(r\_1-\mu)^2}}=\frac{0}{0}=\text{undefined}
$$

这意味着就 GRPO 而言，Qwen-3 32B 的最大上下文长度 6,144 实际上是乘以 2 次生成，即长度为 12,288。

我们下面提供了针对 Llama-3.1 8B 在 LoRA（16 位）和 QLoRA（4 位）两种情况下的实验：

<figure><img src="/files/a58880c3f30abb89fec44e35b05aaf9a6c3a970d" alt="" width="563"><figcaption></figcaption></figure>

**如果您注意到任何训练时间差异，也不会太大**。在我们同条件比较中，我们注意到 <1% 的训练时间减慢或甚至加速，这可能归因于误差范围。

我们还推测，由于内存压力降低，可能会出现提速，因此 CUDA 内存分配器端的内存清理可能减少。

<figure><img src="/files/edf691c3510c53f8993bbb49887bfc992d047e53" alt=""><figcaption></figcaption></figure>

在上图中，您可以看到在单个 T4 GPU 上 Qwen 3 4B 的基线模式与待机模式之间的差异。 <mark style="background-color:green;">**我们可以将 vLLM 的**</mark><mark style="background-color:green;">**&#x20;**</mark><mark style="background-color:green;">**`gpu_memory_utilisation`**</mark><mark style="background-color:green;">**&#x20;**</mark><mark style="background-color:green;">**拉高到高达 0.95，而不用担心会影响训练**</mark>。这意味着您可以容纳更高的上下文长度序列并处理更多序列。例如在第一个案例中，我们有足够的内存来容纳并处理 32K 长度的序列，前提是训练允许，而此前任何超过 2K 的输入可能都无法放入并导致 OOM（内存不足）。

<table data-full-width="true"><thead><tr><th>实验</th><th>配置</th><th>状态</th><th>GPU 内存使用情况</th><th>备注</th></tr></thead><tbody><tr><td><ol><li><a href="https://colab.research.google.com/drive/18CssBY5C0mStnLvu2Hlt4aFLoPugRG0K?usp=sharing">u0.95gen2ga1s Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby True</code></p><p><code>vllm_gpu_util 0.95</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td>运行 40 步 / 40 分钟</td><td><p>14.5 GiB（由 vllm_gpu_util 设置）</p><p><br></p></td><td>足够容纳 32K KVCache，使用 2-4K 的分块，或例如 16K KVCache + 16K 分块</td></tr><tr><td><ol start="2"><li><a href="https://colab.research.google.com/drive/1q0TOUychygfreI2wKpg51sqnRhs5cYnX?usp=sharing">u9ge2ga2s Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby True</code></p><p><code>vllm_gpu_util 0.9</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td>在 40 分钟内运行 32 步</td><td>13.8 GiB（由… 设置）</td><td>大概足够容纳约 28K 的 KVCache，使用 2-4K 的分块，或例如 15K KVCache + 15K 分块</td></tr><tr><td><ol start="3"><li><a href="https://colab.research.google.com/drive/12Uw8y5beLzPtx11mCWCYyh9Z_PEHHdId?usp=sharing">u9ge2ga2ns Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby False</code></p><p><code>vllm_gpu_util 0.9</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td>模型加载但无法训练，因为即使批次大小为 1 也无法放下</td><td>OOM（内存不足）</td><td><br></td></tr><tr><td><ol start="4"><li><a href="https://colab.research.google.com/drive/1GwTlaP5CLsW-BcE1LqZWkz6S8VTWYdJ2?usp=sharing">u8ge2ga2ns Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby False</code></p><p><code>vllm_gpu_util 0.8</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td>模型加载但无法训练，因为即使批次大小为 1 也无法放下</td><td>OOM（内存不足）</td><td><br></td></tr><tr><td><ol start="5"><li><a href="https://colab.research.google.com/drive/1IuSUNzEBTiURK-vbTQuRDuUl0Ya2pz2t?usp=sharing">u7ge2ga2ns Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby False</code></p><p><code>vllm_gpu_util 0.7</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td><p>训练正常</p><p>28 步耗时 39 分钟</p></td><td>约 15.1 GiB</td><td>任何稍长的输入都会在 colab 上导致 OOM</td></tr><tr><td><ol start="6"><li><a href="https://colab.research.google.com/drive/1RY7HwpZ0luJT70OyLJ6zXKZQ2COdT9QJ?usp=sharing">u7gen2ga2s Qwen3_(4B)-GRPO.ipynb</a></li></ol></td><td><p><code>standby True</code></p><p><code>vllm_gpu_util 0.7</code></p><p><code>num_gen 2</code></p><p><code>grad_acc_steps 2</code></p></td><td><p>训练正常</p><p>29 步耗时 40 分钟</p></td><td>13 GiB，但大多数时间在 10–11GB 左右</td><td>在相同配置下，我们节省了 2 GiB，即 15% 的内存。<br>对更长的序列而言节省可能更高</td></tr></tbody></table>

### H100 实验

| 模型                   | GPU                   | 序列长度   | 生成次数 | 梯度累积步数 |
| -------------------- | --------------------- | ------ | ---- | ------ |
| Qwen2.5-14B-Instruct | NVIDIA H100 80GB PCIe | 32,768 | 8    | 4      |

在我们下面可折叠的结果中，您可以看到峰值内存使用存在 9 GiB 的差异（请注意在我们的情况中 90% 的时间 GPU 内存使用等同于峰值内存）。 **作为对比，使用 TRL 和 LoRA 我们最多只能微调一个 8B 参数模型，并且最大上下文长度为 1024（少 32 倍）。** 任何具有更高序列长度的配置（在相似配置下）都会导致进程因 OOM 而失败。

<details>

<summary>点击查看 Unsloth 待机模式与无待机的基准对比</summary>

```
已启用待机模式：

|===========================================================================|
|                  PyTorch CUDA 内存摘要，设备 ID 0                 |
|---------------------------------------------------------------------------|
|            CUDA OOMs: 0            |        cudaMalloc 重试次数: 0         |
|===========================================================================|
|        指标          | 当前使用  | 峰值使用  | 总分配    | 总释放    |
|---------------------------------------------------------------------------|
| 已分配内存          |  32249 MiB |  43042 MiB | 128336 GiB | 128305 GiB |
|       来自大池       |  31415 MiB |  42165 MiB | 127204 GiB | 127173 GiB |
|       来自小池       |    834 MiB |   1184 MiB |   1132 GiB |   1131 GiB |
|---------------------------------------------------------------------------|
| 活跃内存            |  32249 MiB |  43042 MiB | 128336 GiB | 128305 GiB |
|       来自大池       |  31415 MiB |  42165 MiB | 127204 GiB | 127173 GiB |
|       来自小池       |    834 MiB |   1184 MiB |   1132 GiB |   1131 GiB |
|---------------------------------------------------------------------------|
| 请求的内存          |  32199 MiB |  42987 MiB | 128176 GiB | 128145 GiB |
|       来自大池       |  31364 MiB |  42110 MiB | 127047 GiB | 127016 GiB |
|       来自小池       |    834 MiB |   1184 MiB |   1129 GiB |   1128 GiB |
|---------------------------------------------------------------------------|
| GPU 保留内存        |  37644 MiB |  47504 MiB | 705806 MiB | 668162 MiB |
|       来自大池       |  36376 MiB |  46588 MiB | 682818 MiB | 646442 MiB |
|       来自小池       |   1268 MiB |   1284 MiB |  22988 MiB |  21720 MiB |
|---------------------------------------------------------------------------|
| 不可释放内存        | 713142 KiB |   4633 MiB | 103206 GiB | 103205 GiB |
|       来自大池       | 525312 KiB |   4594 MiB | 101923 GiB | 101922 GiB |
|       来自小池       | 187830 KiB |    250 MiB |   1283 GiB |   1283 GiB |
|---------------------------------------------------------------------------|
| 分配次数            |    3460    |    4809    |   15606 K  |   15603 K  |
|       来自大池       |     395    |     563    |    2812 K  |    2811 K  |
|       来自小池       |    3065    |    4270    |   12794 K  |   12791 K  |
|---------------------------------------------------------------------------|
| 活跃分配            |    3460    |    4809    |   15606 K  |   15603 K  |
|       来自大池       |     395    |     563    |    2812 K  |    2811 K  |
|       来自小池       |    3065    |    4270    |   12794 K  |   12791 K  |
|---------------------------------------------------------------------------|
| GPU 保留段数        |     913    |     920    |   13260    |   12347    |
|       来自大池       |     279    |     305    |    1766    |    1487    |
|       来自小池       |     634    |     642    |   11494    |   10860    |
|---------------------------------------------------------------------------|
| 不可释放分配次数    |     422    |     628    |    4766 K  |    4765 K  |
|       来自大池       |      66    |      92    |    1290 K  |    1289 K  |
|       来自小池       |     356    |     555    |    3476 K  |    3475 K  |
|---------------------------------------------------------------------------|
| 超大分配            |       0    |       0    |       0    |       0    |
|---------------------------------------------------------------------------|
| 超大 GPU 段         |       0    |       0    |       0    |       0    |
|===========================================================================|


未启用待机：

|===========================================================================|
|                  PyTorch CUDA 内存摘要，设备 ID 0                 |
|---------------------------------------------------------------------------|
|            CUDA OOMs: 0            |        cudaMalloc 重试次数: 0         |
|===========================================================================|
|        指标          | 当前使用  | 峰值使用  | 总分配    | 总释放    |
|---------------------------------------------------------------------------|
| 已分配内存          |  32711 MiB |  52084 MiB | 142756 GiB | 142724 GiB |
|       来自大池       |  31877 MiB |  51207 MiB | 141499 GiB | 141467 GiB |
|       来自小池       |    834 MiB |   1184 MiB |   1257 GiB |   1256 GiB |
|---------------------------------------------------------------------------|
| 活跃内存            |  32711 MiB |  52084 MiB | 142756 GiB | 142724 GiB |
|       来自大池       |  31877 MiB |  51207 MiB | 141499 GiB | 141467 GiB |
|       来自小池       |    834 MiB |   1184 MiB |   1257 GiB |   1256 GiB |
|---------------------------------------------------------------------------|
| 请求的内存          |  32572 MiB |  51658 MiB | 141898 GiB | 141866 GiB |
|       来自大池       |  31738 MiB |  50780 MiB | 140644 GiB | 140613 GiB |
|       来自小池       |    833 MiB |   1184 MiB |   1253 GiB |   1252 GiB |
|---------------------------------------------------------------------------|
| GPU 保留内存        |  49552 MiB |  52188 MiB |  86354 MiB |  36802 MiB |
|       来自大池       |  48320 MiB |  51300 MiB |  84740 MiB |  36420 MiB |
|       来自小池       |   1232 MiB |   1232 MiB |   1614 MiB |    382 MiB |
|---------------------------------------------------------------------------|
| 不可释放内存        |      0 B   |      0 B   |      0 B   |      0 B   |
|       来自大池       |      0 B   |      0 B   |      0 B   |      0 B   |
|       来自小池       |      0 B   |      0 B   |      0 B   |      0 B   |
|---------------------------------------------------------------------------|
| 分配次数            |    3460    |    4809    |   17440 K  |   17437 K  |
|       来自大池       |     395    |     564    |    2742 K  |    2741 K  |
|       来自小池       |    3065    |    4270    |   14698 K  |   14695 K  |
|---------------------------------------------------------------------------|
| 活跃分配            |    3460    |    4809    |   17440 K  |   17437 K  |
|       来自大池       |     395    |     564    |    2742 K  |    2741 K  |
|       来自小池       |    3065    |    4270    |   14698 K  |   14695 K  |
|---------------------------------------------------------------------------|
| GPU 保留段数        |       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    |
|---------------------------------------------------------------------------|
| 超大 GPU 段         |       0    |       0    |       0    |       0    |
|===========================================================================|
```

</details>

下图显示了待机与非待机训练在 Unsloth 中的比较。数据取三次运行的平均以确保指标不噪声化。事实上，如果放大观察，您会发现启用待机还能使其更快，这很可能如前所述是由于内存压力减少。

<figure><img src="/files/cde5bdacf20ed87ab04f8a088e1833952ee76477" alt=""><figcaption></figcaption></figure>

### 之前的 A100 40GB 实验

在我们之前针对 A100 40GB GPU、使用 Qwen-2.5-3b-instruct 且每个样本 8 次生成的实验中，我们观察到在未启用待机时，GRPO 训练（模型以 16 位加载、LoRA、仅权重可训练）最多只能容纳 6K 的序列长度。使用我们的待机功能，我们能够容纳 10K 及更高！ **相比之下，TRL 在保持相同批次大小的情况下最多只能给出 1K 的上下文长度。**

<figure><img src="/files/7c92544ec3f2ad7dd560a440f48159e21564e495" alt="" width="563"><figcaption></figcaption></figure>

## :tada:其他优化

我们现在选择了更好的编译标志并将编译时间减少了 50% 或更多。我们还成功对任意 vLLM 版本进行动态补丁以处理 `gc.collect` 为向后兼容性做得更好，灵感来自这个 [vLLM pull request（拉取请求）](https://github.com/vllm-project/vllm/pull/21146)。这将编译时间从 2 分钟缩短到不到 40 秒。

我们还对 `torch.compile` 标志进行了优化并尝试开启某些标志——不幸的是 `combo_kernels` 和 `multi_kernel` 在 vLLM 0.10 与 Torch 2.8/2.9 nightly 上无法正常工作，并且 `coordinate_descent_tuning` 使得所有内核的自动调优显著变慢。以前编译不到一分钟，但启用后耗时超过 13 分钟以上，且性能提升很小。

## :books:GRPO 笔记本

我们所有的 GRPO 笔记本默认开启 Unsloth Standby 并应用所有优化！请参见 <https://docs.unsloth.ai/get-started/unsloth-notebooks> 获取我们所有的 GRPO 笔记本，或试试下面的：

* [**Qwen3（4B）**](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_\(4B\)-GRPO.ipynb) **-** 高级 GRPO LoRA
* [**DeepSeek-R1-0528-Qwen3（8B）**](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/DeepSeek_R1_0528_Qwen3_\(8B\)_GRPO.ipynb) （用于多语言用例）
* [Gemma 3（1B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3_\(1B\)-GRPO.ipynb)
* [Llama 3.2（3B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Advanced_Llama3_2_\(3B\)_GRPO_LoRA.ipynb) - 高级 GRPO LoRA
* [Llama 3.1（8B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.1_\(8B\)-GRPO.ipynb)
* [Phi-4（14B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Phi_4_\(14B\)-GRPO.ipynb)
* [Mistral v0.3（7B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Mistral_v0.3_\(7B\)-GRPO.ipynb)
* [Qwen2.5（3B）](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen2.5_\(3B\)-GRPO.ipynb)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://unsloth.ai/docs/zh/kai-shi-shi-yong/reinforcement-learning-rl-guide/memory-efficient-rl.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
