Prhub

#6906 [rollout] fix: support SGLang FP8 ignored layers for Qwen3.x GatedDeltaNet in rollout

原始 PR 作者 gem-mint 合并时间 2026-07-02 10:35 文件变更 4 提交数 4 评论 2 代码增减 +219 / -14

执行摘要

SGLang rollout FP8 忽略层配置集中化并支持正则匹配

SGLang 0.5.12 支持 FP8 ignored_layers,但 verl 的 SGLang rollout 路径内联构建 FP8 配置,且 SGLangFP8QuantizerHelper 未在 update_weights 前应用相同忽略规则,导致 Qwen GatedDeltaNet linear_attn 投影层因 tensor-parallel 后局部维度不整除 [128,128] 权重块而崩溃。本 PR 对齐 server 端和 verl-side 的忽略规则,通过集中配置生成避免该问题。

值得精读。设计上通过工具函数将多种配置源融合为统一忽略列表,并支持正则匹配以兼容 llm-compressor 等工业工具生成的规则。这种模式可以推广到其他推理引擎(如 vLLM)的相似场景。但需注意测试的边界条件(如重叠规则、空字符串、None 等)已被覆盖,可以放心依赖。

讨论亮点

Review 中 gemini-code-assist[bot] 指出初始版本缺少对 re: 正则前缀的支持(_matches_ignored_layer 只有字符串匹配),这会导致使用 llm-compressor 量化(常见生成 re:.*linear_attn.* 规则)的模型出现运行时崩溃。该评论被标记为 high priority。作者在后续提交(126e087)中添加了正则分支支持:在匹配前检查 ignored_layer.startswith('re:'),提取模式并用 re.match 匹配参数名和模块名。该线程已解决。

此外,审核者 wuxibin89 在 PR 评论中要求修复 pre-commit 格式问题(import 顺序),作者在最后一个提交(fdd0e3c)中调整了导入顺序解决。最终获得批准。

实现拆解

  1. 新增辅助工具函数:在 verl/utils/sglang/sglang_fp8_utils.py 中新增 _get_config_value(兼容 dict 和 MappingLike 对象)、_normalize_ignored_layers(支持字符串逗号分割、列表、单个元素归一化)、_dedupe_layers(大小写不敏感去重)、get_sglang_fp8_ignored_layers(合并来自 HF 配置中 ignored_layersmodules_to_not_convert 以及环境变量 SGLANG_FP8_IGNORED_LAYERS 的忽略列表)和 _matches_ignored_layer(支持精确路径匹配、前缀/后缀/子串匹配以及 re: 正则匹配)。

  2. 集中化配置构建build_sglang_fp8_quant_config 函数接收 HF 配置和额外忽略列表,构建包含 activation_scheme, fmt, weight_block_size 等默认字段的 block-wise FP8 字典,并调用 get_sglang_fp8_ignored_layers 合并所有忽略层来源,最后去重后写入 ignored_layers。该函数供 server 启动和权重同步共同调用。

  3. 更新 QuantizerHelper:在 SGLangFP8QuantizerHelper__init__ 中调用 get_sglang_fp8_ignored_layers 保存忽略列表;新增 should_quantize_param 方法,遍历忽略列表并调用 _matches_ignored_layer 判断参数名是否匹配,匹配则返回 False(跳过量化),否则调用父类原始逻辑。

  4. 替换硬编码配置:在 async_sglang_server.pysglang_rollout.py 中删除原有的内联 FP8 配置字典(FP8_BLOCK_QUANT_KWARGS),改为导入并调用 build_sglang_fp8_quant_config(self.model_config.hf_config),结果赋值给 fp8_block_quant_kwargs,再通过 json_model_override_args 传给 SGLang server 或写入 quantization_config

  5. 配套测试:新增 tests/utils/test_sglang_fp8_utils.py,覆盖默认输出、HF 配置合并 ignored_layers/modules_to_not_convert、MappingLike 输入兼容、正则匹配正则忽略层、环境变量读取等五个场景。使用 monkeypatch 模拟环境变量,SimpleNamespace 和自定义 MappingLikeConfig 模拟 HF 配置对象。

文件 模块 状态 重要度
verl/utils/sglang/sglang_fp8_utils.py 量化工具 modified 8.45
tests/utils/test_sglang_fp8_utils.py 单元测试 added 7.62
verl/workers/rollout/sglang_rollout/async_sglang_server.py rollout 服务 modified 5.71
verl/workers/rollout/sglang_rollout/sglang_rollout.py rollout 服务 modified 5.71

关键符号

_get_config_value _normalize_ignored_layers _dedupe_layers _get_ignored_layers_from_env get_sglang_fp8_ignored_layers _matches_ignored_layer build_sglang_fp8_quant_config should_quantize_param

关键源码片段

verl/utils/sglang/sglang_fp8_utils.py core-logic

核心实现文件,新增了所有忽略层处理逻辑和集中式配置构建,是 PR 的核心变更。

def build_sglang_fp8_quant_config(hf_config: Any = None, ignored_layers: Any = None) -> dict[str, Any]:
    # 构建 SGLang block-wise FP8 基础配置,与 server 端保持一致
    fp8_quant_config = {
        'activation_scheme': 'dynamic',
        'fmt': 'e4m3',
        'quant_method': 'fp8',
        'weight_block_size': [128, 128],
    }
​
    # 从 HF 量化配置中提取字段,兼容 dict 和 OmegaConf 等 MappingLike 对象
    hf_quant_config = _get_config_value(hf_config, 'quantization_config')
    # 合并三种来源的忽略层:HF 配置中的 ignored_layers 和 modules_to_not_convert,
    # 再加上环境变量 SGLANG_FP8_IGNORED_LAYERS
    merged_ignored_layers = get_sglang_fp8_ignored_layers(hf_quant_config)
    # 额外接收通过参数传入的忽略层(供给调用方覆盖)
    merged_ignored_layers.extend(_normalize_ignored_layers(ignored_layers))
    # 大小写不敏感去重,保留首次出现的顺序
    merged_ignored_layers = _dedupe_layers(merged_ignored_layers)
    if merged_ignored_layers:
        fp8_quant_config['ignored_layers'] = merged_ignored_layers
​
    return fp8_quant_config
​
​
def _matches_ignored_layer(param_name: str, ignored_layer: str) -> bool:
    # 判断参数名是否应被忽略量化
    ignored_layer = ignored_layer.strip()
    if not ignored_layer:
        return False
​
    name = param_name.strip('.')
    # 提取模块名:去除末尾的 '.weight' 后缀(如果有)
    module_name = name[: -len('.weight')] if name.lower().endswith('.weight') else name
​
    # 支持 're:' 前缀的正则表达式模式
    if ignored_layer.startswith('re:'):
        pattern = ignored_layer[3:]
        # 同时匹配完整参数名和模块名
        return any(re.match(pattern, candidate) for candidate in (name, module_name))
​
    # 字符串匹配:忽略大小写和首尾点,检查精确、前缀、后缀、子串关系
    ignored_layer = ignored_layer.lower().strip('.')
    name = name.lower()
    module_name = module_name.lower()
    for candidate in (name, module_name):
        if candidate == ignored_layer:
            return True
        if candidate.startswith(f'{ignored_layer}.'):
            return True
        if candidate.endswith(f'.{ignored_layer}'):
            return True
        if f'.{ignored_layer}.' in f'.{candidate}.':
            return True
    return False
​
​
class SGLangFP8QuantizerHelper(FP8QuantizerHelper):
    def __init__(self, quant_config):
        super().__init__(quant_config)
        # 从量化配置中提取忽略层列表,合并所有来源
        self.ignored_layers = get_sglang_fp8_ignored_layers(quant_config)
​
    def should_quantize_param(self, param_name):
        # 如果参数名匹配任一忽略层,则跳过量化
        for ignored_layer in self.ignored_layers:
            if _matches_ignored_layer(param_name, ignored_layer):
                return False
        # 否则执行父类的默认量化判断
        return super().should_quantize_param(param_name)
tests/utils/test_sglang_fp8_utils.py test-coverage

新增完整测试套件,覆盖默认配置、HF 忽略层合并、MappingLike 兼容、正则匹配、环境变量读取等关键场景。

def test_sglang_fp8_quantizer_matches_regex_ignored_layers(monkeypatch):
    # 清除环境变量,确保只从 HF 配置读取
    monkeypatch.delenv('SGLANG_FP8_IGNORED_LAYERS', raising=False)
​
    # 模拟 HF 配置对象,其中量化配置包含正则忽略层
    hf_config = SimpleNamespace(
        quantization_config={
            'ignored_layers': ['re:.*linear_attn.*'],
        }
    )
​
    # 构建集中式配置
    quant_config = build_sglang_fp8_quant_config(hf_config)
    # 创建量化助手,内部会解析忽略列表
    helper = SGLangFP8QuantizerHelper(quant_config)
​
    # 验证正则匹配生效
    assert quant_config['ignored_layers'] == ['re:.*linear_attn.*']
    # 属于 linear_attn 的参数应当跳过量化
    assert not helper.should_quantize_param('model.layers.0.linear_attn.in_proj_ba.weight')
    assert not helper.should_quantize_param('model.layers.0.linear_attn.g_proj.weight')
    # 其他层参数应正常量化
    assert helper.should_quantize_param('model.layers.0.mlp.experts.0.up_proj.weight')

评论区精华

支持正则表达式忽略层匹配 正确性

gemini-code-assist[bot] 指出初始版本 _matches_ignored_layer 只支持字符串前缀 / 后缀匹配,不支持 re: 正则前缀,会导致 llm-compressor 量化模型崩溃。

结论:作者在第 3 个 commit(126e087)中添加了 `re:` 前缀检测和 `re.match` 调用,支持正则忽略层匹配。 · 已解决

修复 pre-commit import 排序 style

wuxibin89 要求修复 pre-commit 格式失败,具体是 import 顺序。

结论:作者在最后一个 commit(fdd0e3c)中调整导入顺序,将外部导入放在标准库导入之后。 · 已解决

风险与影响

  1. 正则表达式性能风险_matches_ignored_layer 在每次 should_quantize_param 调用时都会对每个忽略层尝试 re.match,如果模型参数量大且忽略层为复杂正则,可能增加权重同步耗时。但该路径仅在 update_weights 时触发,不在推理热路径,可接受。

  2. 配置合并优先级不确定:当多个来源(HF 配置的 ignored_layersmodules_to_not_convert、环境变量)同时指定时,当前去重策略保留首次出现的条目,可能导致用户预期外行为(例如环境变量覆盖 HF 配置失败)。但文档明确说明合并顺序为:HF ignored_layers → modules_to_not_convert → 环境变量,且去重基于小写后首次出现,与直觉一致。

  3. 环境变量安全SGLANG_FP8_IGNORED_LAYERS 环境变量可能被无意设置,导致模型部分层跳过 FP8 量化,影响精度。但该变量与 SGLang server 共享,纳入后反而避免了两端不一致的更大风险。

  4. 向后兼容性:未使用忽略层的旧配置不受影响,get_sglang_fp8_ignored_layers 在无忽略层时返回空列表,build_sglang_fp8_quant_config 不包含 ignored_layers 键,行为与原始硬编码配置一致。

用户影响:修复了 Qwen3.x GatedDeltaNet 等模型在使用 SGLang rollout + FP8 量化时因维度不匹配导致的崩溃。用户可通过 HF 配置的 ignored_layersmodules_to_not_convert 或设置 SGLANG_FP8_IGNORED_LAYERS 环境变量来控制忽略层。

系统影响:无显著性能回归,仅权重同步阶段增加少量字符串匹配和正则匹配。

团队影响:集中 FP8 配置生成逻辑,后续 SGLang 新增量化配置项时只需修改 build_sglang_fp8_quant_config 一处,降低维护成本。

正则匹配性能开销 配置合并优先级可能不明显 环境变量可能意外生效

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论