Prhub

#32031 [NPU]Fix run_lora_a_embedding out-of-vocab token produces wrong embedding.

原始 PR 作者 qinsir5522 合并时间 2026-08-25 14:09 文件变更 1 提交数 17 评论 3 代码增减 +41 / -0

执行摘要

修复 NPU 上 LoRA 嵌入越界 token 崩溃

PR body 中给出了完整崩溃栈:Scheduler 在 run_lora_a_embedding 后调用 run_lora_b_sgemm 时抛出 AttributeError: 'NoneType' object has no attribute 'shape'。根因是 BaseLoRABackend.run_lora_a_embedding 只有空实现(pass)且未返回有效值,而 AscendLoRABackend 未覆写该方法,导致 None 被传入后续 sgemm 流程。同时,越界 token id 会产生错误的 embedding,PR 目标就是在 NPU 后端提供与 torch_native 等价的 LoRA-A embedding 查找能力。

值得阅读,尤其是对 LoRA 后端实现和 NPU 算子适配感兴趣的工程师;全向量化 3D 高级索引替代逐段循环的思路可复用到其它后端。建议补充单元测试(包含越界 token、空权重、rank 掩码边界),并清理未使用的 num_loras 变量。

讨论亮点

该 PR 没有实质的人工 review 讨论。唯一的审核记录是 sglang-npu-bot 的 APPROVED,并且 bot 在 issue 区域两次执行 /tag-and-rerun-ci 重新触发 CI,未留下代码层面的讨论或修改意见。

实现拆解

  1. 问题定位:确认崩溃发生在 ascend_backend.py 的 AscendLoRABackend 类中,由于未覆写 run_lora_a_embedding,基类空方法返回 None,导致 run_lora_b_sgemm 解包失败。
  2. 新增实现:在 AscendLoRABackend 中新增 run_lora_a_embedding(input_ids, weights, vocab_size, extra_embeddings=None, ...),采用一次性 3D advanced indexing 而非逐 segment 循环,更适配 NPU 执行。
  3. 关键逻辑
    • 断言 extra_embeddings 为空,空权重直接返回零张量;
    • 使用 clamp(0, vocab_size_w - 1) 将越界 token id 拉回合法区间;
    • 通过 repeat_interleave 将 batch_info.weight_indices 按 seg_lens 展开为逐 token 的 LoRA 索引;
    • 构造展开后的 token/rank/vocab 索引张量,从 weights [num_loras, max_rank, vocab_size] 中 gather 结果;
    • 用 rank_mask 屏蔽超过实际 rank 的列,再乘以 scalings 完成缩放。
  4. 配套验证:PR body 报告了 GSM8K 精度测试(accuracy 0.735),但仓库内未新增针对该方法的单元测试,属于独立 bugfix 合入。
文件 模块 状态 重要度
python/sglang/srt/lora/backend/ascend_backend.py LoRA 后端 modified 6.77

关键符号

run_lora_a_embedding

关键源码片段

python/sglang/srt/lora/backend/ascend_backend.py core-logic

修复的核心文件:为 AscendLoRABackend 新增缺失的 run_lora_a_embedding 实现,解决 NPU LoRA embedding 崩溃与越界 token 错误 embedding 问题。

def run_lora_a_embedding(
    self, input_ids, weights, vocab_size, extra_embeddings=None, *args, **kwargs
):
    # Ascend 后端暂不支持附加 token 的额外 embedding,显式拦截避免歧义
    assert (
        extra_embeddings is None
    ), "Ascend LoRA embedding backend does not support extra embeddings (added tokens)."
​
    total_seq_len = input_ids.shape[0]
​
    # 空权重(如未加载任何 LoRA)时直接返回零张量,避免后续 gather 越界
    if weights.numel() == 0:
        return torch.zeros(
            total_seq_len, 0, device=input_ids.device, dtype=weights.dtype
        )
​
    # weights 布局为 [num_loras, max_rank, vocab_size],注意 num_loras 在本实现中未使用
    num_loras, max_rank, vocab_size_w = weights.shape
    # 将 out-of-vocab 的 token id 收敛到合法区间,保证索引安全
    clamped_ids = input_ids.clamp(0, vocab_size_w - 1).to(torch.int64)
​
    # 按 segment 长度把每个 token 映射到所属 LoRA 的权重索引
    token_lora_idx = torch.repeat_interleave(
        self.batch_info.weight_indices.to(torch.int64),
        self.batch_info.seg_lens,
        output_size=total_seq_len,
    )
​
    rank_per_token = self.batch_info.lora_ranks[token_lora_idx]
    scaling_per_token = self.batch_info.scalings[token_lora_idx]
    rank_idx = torch.arange(max_rank, device=weights.device)
​
    # 3D 高级索引:将 token 维、rank 维、vocab 维同时展开后一次性 gather
    token_lora_idx_expanded = token_lora_idx.unsqueeze(1).expand(
        total_seq_len, max_rank
    )
    rank_idx_expanded = rank_idx.unsqueeze(0).expand(total_seq_len, max_rank)
    clamped_ids_expanded = clamped_ids.unsqueeze(1).expand(total_seq_len, max_rank)
​
    result = weights[
        token_lora_idx_expanded, rank_idx_expanded, clamped_ids_expanded
    ]
​
    # 实际 rank 不足 max_rank 的位置置零,保持与逐段 sgemm 相同的填充语义
    rank_mask = rank_idx.unsqueeze(0) < rank_per_token.unsqueeze(1)
    result = result * rank_mask.to(result.dtype)
​
    # 应用每个 token 对应的 LoRA 缩放因子
    result = result * scaling_per_token.unsqueeze(1).to(result.dtype)
​
    return result

评论区精华

评审与 CI 流程 other

PR 没有人工 review 评论,只有 sglang-npu-bot 自动批准并在 issue 区域两次执行 /tag-and-rerun-ci 重新触发 CI。

结论:机器人流程通过后 PR 被直接合并,未经人工代码评审。 · closed

风险与影响

  1. 高风险点:无人工评审,仅机器人自动批准;缺少单元测试覆盖越界 token、空权重、rank 掩码等边界条件。
  2. 语义风险:clamp 会让越界 token 静默指向最后一个 vocab 条目,可能掩盖上游数据错误,需要确认是否符合产品预期。
  3. 性能与显存风险:3D 索引展开后 result 形状为 [total_seq_len, max_rank],当 max_rank 较大时中间张量和最终结果会占用更多显存;相比逐段循环,优点是算子次数更少、更适合 NPU 执行。
  4. 代码质量:num_loras 变量赋值后未使用,属于遗留死代码,建议清理。

影响范围限于 Ascend NPU 上启用 LoRA embedding 的推理用户(如 --lora-backend ascend 场景),修复了崩溃并保证越界 token 的 embedding 正确性。其它 LoRA 后端(如 torch_native)不受影响,因为变更仅在 AscendLoRABackend 内新增方法。对团队而言,该 PR 补齐了 NPU LoRA 路径上的功能缺口,但需要后续补充测试和人工评审。

缺少单元测试 无人工评审 clamp 语义可能掩盖越界数据错误 高级索引展开有显存峰值风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论