Prhub

#7428 [fsdp] fix: skip no-op unit temperature scaling

原始 PR 作者 Mengyuyang 合并时间 2026-09-01 13:56 文件变更 2 提交数 3 评论 0 代码增减 +93 / -3

执行摘要

FSDP 引擎跳过温度为 1 时的全词表 logits 缩放,降低峰值内存。

动机源于 PR#6935 将温度缩放改为 out-of-place 操作(因为 output.logits.squeeze(0) 可能返回自定义 autograd 函数的视图),但该操作在温度为 1 时仍会分配一个与 logits 大小相同的 tensor,造成了不必要的内存开销(对大词表模型尤为明显)。本 PR 旨在消除这一特定场景下的无谓分配,同时不破坏 autograd 安全性和其他温度配置的行为。PR body 明确指出:"This PR avoids an unnecessary full-vocabulary logits allocation in the FSDP eager log-probability path when the configured temperature is a host scalar equal to 1."

建议精读。这是一个小范围但设计精良的性能优化 PR。它清晰地识别了一个可以安全跳过的计算(温度=1 时的恒等缩放),通过引入清晰的辅助函数和状态传递实现了优化,同时配套了完备的测试和容错处理。其“识别并跳过 no-op”的优化思路和“保持兼容性”的实现方式,对于处理类似场景具有参考价值。

讨论亮点

无 review 评论。从提交历史看,第二个提交信息为“tolerate missing temperature metadata”,表明作者在实现后意识到有内部调用方可能直接构造 output_args 而不经过 prepare_model_inputs,因此在 prepare_model_outputs 中增加了对 temperature_is_one 键的 get 方法容错处理,确保向后兼容性。这是一个重要的防御性编程实践。

实现拆解

  1. 引入检测与缩放辅助函数:在 verl/workers/engine/fsdp/transformer_impl.py 文件顶部新增 _is_scalar_unit_temperature(temperature)_scale_logits_by_temperature(logits, temperature, is_unit_temperature) 两个模块级辅助函数。前者严格检测非 Tensor 的宿主标量且值为 1.0 的情况;后者根据 is_unit_temperature 标志决定是返回原始 logits 还是执行除法缩放。
  2. prepare_model_inputs 中标记并传递状态:在 FSDPEngineWithLMHead.prepare_model_inputs 方法中,于温度被转换为张量之前,调用 _is_scalar_unit_temperature 获取布尔标志 temperature_is_one,并将其存入 output_args 字典(键为 "temperature_is_one"),以便在后续输出处理阶段使用。
  3. prepare_model_outputs 中应用优化路径:在 FSDPEngineWithLMHead.prepare_model_outputs 方法中,从 output_args 获取 temperature_is_one 标志(并处理调用方可能未提供该标志的容错情况,回退到旧路径)。在 remove-padding 和非 remove-padding 两个分支的 logits 缩放位置,用调用 _scale_logits_by_temperature 替换原有的直接除法操作。
  4. 新增全面的单元测试:新增 tests/workers/test_fsdp_temperature_scaling_on_cpu.py 测试文件,覆盖对辅助函数的检测逻辑、单位温度下原始 logits 存储和梯度保持、以及非单位温度下 out-of-place 行为和梯度正确性的验证。
文件 模块 状态 重要度
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 7.69
tests/workers/test_fsdp_temperature_scaling_on_cpu.py FSDP 测试 added 7.46

关键符号

_is_scalar_unit_temperature _scale_logits_by_temperature FSDPEngineWithLMHead.prepare_model_inputs FSDPEngineWithLMHead.prepare_model_outputs

关键源码片段

verl/workers/engine/fsdp/transformer_impl.py core-logic

核心源码文件,包含温度缩放优化逻辑的定义和应用点。

# verl/workers/engine/fsdp/transformer_impl.py# 新增的模块级辅助函数def _is_scalar_unit_temperature(temperature) -> bool:
    """Return whether a host scalar temperature makes scaling a no-op."""
    # 仅当温度是非张量的 Python 标量且值为 1.0 时返回 True
    # 张量温度(即使值为 1)会走通用路径,避免设备同步并保留 per-sample 语义
    return not isinstance(temperature, torch.Tensor) and float(temperature) == 1.0
​
​
def _scale_logits_by_temperature(logits, temperature, *, is_unit_temperature: bool):
    """Scale logits without copying the full vocabulary tensor for temperature 1."""
    if is_unit_temperature:
        # 温度为 1,恒等操作,直接返回原始 logits 引用,避免内存分配
        return logits
    # 其他情况,执行安全的 out-of-place 除法缩放
    return logits / temperature.clamp(min=1e-8).to(logits.dtype)
​
​
# 在 FSDPEngineWithLMHead.prepare_model_inputs 中的应用片段
    temperature = micro_batch["temperature"]
    temperature_item = temperature
    # 在转换为张量前,检测是否为单位温度
    temperature_is_one = _is_scalar_unit_temperature(temperature)
    # ... ( 后续代码不变 ) ...
​
    # 将标志存入 output_args 供后续使用
    output_args = {"temperature_is_one": temperature_is_one}
​
​
# 在 FSDPEngineWithLMHead.prepare_model_outputs 中的应用片段
    # 从 output_args 获取标志,若缺失则默认为 False(兼容直接构造 output_args 的调用方)
    temperature_is_one = output_args.get("temperature_is_one", False)
​
    # ...
    # 在 remove-padding 分支
    logits_rmpad = output.logits.squeeze(0)
    if isinstance(logits_rmpad, DTensor):
        logits_rmpad = logits_rmpad.full_tensor()
    # 使用辅助函数替代直接除法
    logits_rmpad = _scale_logits_by_temperature(
        logits_rmpad,
        temperature_rmpad.unsqueeze(-1),
        is_unit_temperature=temperature_is_one,
    )
​
    # 在非 remove-padding 分支
    logits = output.logits
    # ... ( 收集 DTensor) ...
    logits = _scale_logits_by_temperature(
        logits,
        temperature,
        is_unit_temperature=temperature_is_one,
    )
tests/workers/test_fsdp_temperature_scaling_on_cpu.py test-coverage

新增的测试文件,为辅助函数提供单元测试覆盖,是保证变更正确性的关键。

# tests/workers/test_fsdp_temperature_scaling_on_cpu.py
import pytest
import torch
from verl.workers.engine.fsdp.transformer_impl import (
    _is_scalar_unit_temperature,
    _scale_logits_by_temperature,
)# 测试标量单位温度检测
@pytest.mark.parametrize("temperature", [1, 1.0])
def test_scalar_unit_temperature_is_detected(temperature):
    assert _is_scalar_unit_temperature(temperature) is True
​
​
# 测试非单位温度或张量温度应走通用路径
@pytest.mark.parametrize("temperature", [0.7, 2.0, torch.tensor(1.0)])
def test_non_unit_or_tensor_temperature_uses_general_path(temperature):
    assert _is_scalar_unit_temperature(temperature) is False
​
​
# 测试单位温度保持 logits 存储位置和梯度def test_unit_temperature_preserves_logits_storage_and_gradient():
    base = torch.randn(1, 4, 8, requires_grad=True)
    logits_view = base.squeeze(0) # 创建一个视图
    scaled = _scale_logits_by_temperature(
        logits_view,
        torch.ones(4, 1),
        is_unit_temperature=True,
    )
    # 验证返回的是同一个张量对象(无拷贝)
    assert scaled is logits_view
    # 验证梯度可以正确回传到 base
    scaled.sum().backward()
    torch.testing.assert_close(base.grad, torch.ones_like(base))
​
​
# 测试非单位温度的 out-of-place 行为和梯度def test_non_unit_temperature_is_out_of_place_and_has_correct_gradient():
    logits = torch.randn(4, 8, requires_grad=True)
    temperature = torch.full((4, 1), 0.5)
    scaled = _scale_logits_by_temperature(
        logits,
        temperature,
        is_unit_temperature=False,
    )
    # 验证创建了新的张量(非就地修改)
    assert scaled is not logits
    # 验证缩放结果正确 (logits / 0.5 = logits * 2)
    torch.testing.assert_close(scaled, logits * 2)
    # 验证梯度正确 (d(scaled)/d(logits) = 2)
    scaled.sum().backward()
    torch.testing.assert_close(logits.grad, torch.full_like(logits, 2))

评论区精华

容错性设计 设计

提交历史显示第二个提交信息为“tolerate missing temperature metadata”,表明作者意识到了有内部调用方可能直接构造 `output_args` 而不经过 `prepare_model_inputs`。

结论:在 `prepare_model_outputs` 中使用 `output_args.get("temperature_is_one", False)` 来容错处理缺失的元数据,确保向后兼容和路径安全。 · 已解决

风险与影响

  1. 正确性风险:核心风险在于 _is_scalar_unit_temperature 的判断逻辑。若判断为 True 但实际温度不为 1,将错误跳过缩放,导致 log-prob 计算错误,影响 RL 训练稳定性。测试文件通过参数化覆盖了 1, 1.0 为真,0.7, 2.0, torch.tensor(1.0) 为假的场景,基本覆盖了关键边界。
  2. 兼容性风险:通过在 output_args 中传递新键值 temperature_is_one,并使用 get(..., False) 容错,确保了与直接构造 output_args 的内部调用方的向后兼容。变更未影响公共 API 或配置。
  3. 回归风险:变更范围局限于 FSDP eager 路径的温度缩放逻辑。对其他路径(如 fused kernels)或温度配置(如张量温度、per-sample温度)无影响。配套测试确保了核心行为。

对用户:当使用 FSDP 引擎且配置 temperature: 1(常见于策略梯度计算)时,可以减少一次与整个词表大小相同的张量内存分配,从而降低训练过程中的峰值内存占用,尤其对词表规模大的模型有益。
对系统:影响范围限定在 FSDP 引擎的 log-prob 计算路径,不影响其他组件(如 vLLM rollout、分布式训练逻辑)。
对团队:提供了一个清晰的“恒等操作短路”优化模式,在保持 autograd 安全性和向后兼容的前提下,实现了针对性的性能提升。

核心路径变更 内存分配优化

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论