当中断绑核遇上大模型推理:HostBound 问题优化全解析(昇腾深度实战版)


在大模型推理性能调优的过程中,我们经常会遇到一个令人头疼的现象------HostBound 。CPU 明明没有满载,但推理速度就是慢,Profiling 一看,全是"空泡"。 调试分析之后有点发现,最终我发现问题是在一个经常被忽略的角落:IRQ** 中断机制与中断绑核**。

因此本文结合昇腾平台的实际测试,从 IRQ 工作机制、irqbalance 调度策略,到如何通过绑核优化推理性能,完整还原这次 HostBound 问题的排查与优化过程。


一、背景:从 HostBound 说起

在大模型推理时,Host 侧算子进入下发队列后,会依次触发两个关键中断事件:

  • sq_send_trigger_irq:算子下发触发;
  • cq_update_irq:算子执行完成后上报。

这两个中断的发生 CPU 核通常是固定的。 而如果此时业务线程恰好也绑定在相同的 CPU 核上,就会出现频繁的任务打断,导致 CPU 空泡显著增加,形成典型的 HostBound 问题

在默认情况下,如果系统安装并启用了 irqbalance 服务,它会定期(默认 10 秒)重新分配中断,使中断均衡分布在多个 CPU 上。 但对于推理这种极端计算密集型场景,这种"自动平衡"反而可能带来性能抖动甚至劣化。


为什么在昇腾平台上 HostBound 更明显?

与 GPU 不同,昇腾 NPU 采用 Host/NPU 协同执行架构。在整个模型推理过程中,CPU 承担了以下关键角色:

  1. 算子编排、调度与 TaskGen(CANN Runtime)
  2. 向 AscendCL/DevDrv 下发任务,触发 sq_send_trigger_irq
  3. 等待 NPU 完成 cq_update_irq 回调,用于同步下一步调度
  4. 部分算子的 Host 侧逻辑,如 shape 计算、LayerNorm 前置操作等

昇腾框架(CANN + torch-npu)比 GPU 更依赖 CPU 的调度链路,因此 CPU 不仅需要供能(Host Control),还承担巨量 Host 侧同步逻辑。

NPU ** 每执行一次算子 → 至少两次 IRQ:**

  • sq_send_trigger_irq(下发)
  • cq_update_irq(完成)

而大模型推理 每生成一个 token 会触发数百到上千个算子调度,即会产生成千上万次中断。

因此,昇腾平台比 GPU 更容易出现一个现象:

CPU 被高频中断打断,导致 HostBound,即使 CPU 利用率不高,也会出现严重空泡。


二、了解 IRQ 与 irqbalance 的机制

1. 什么是硬件中断?

**中断(Interrupt)**是一种"打断当前程序执行"的机制。 当外设(如 NPU、网卡、定时器)有事件发生时,会通知 CPU 中断当前任务,执行一段专门的"中断服务程序(ISR)"。 这样可以保证 CPU 能及时响应设备请求,而不是傻等。

举个例子,当 NPU 执行完算子后,需要告诉 CPU "我完成了",这个信号就是一次中断。 如果中断太频繁,CPU 就会被不断"打断",算子调度效率明显下降。


2. 中断的注册与维护

设备驱动加载时会通过 request_irq() 注册中断,系统会分配一个 IRQ ID。 所有中断状态都可以在 /proc/interrupts 中查看,例如:

IRQ** ID** CPU0 CPU1 CPU2 CPU3 控制器类型 硬件中断号 触发方式 中断源**/设备**
8 0 0 0 0 GICv3 22 Level vgic
9 0 12 0 0 GICv3 28 Level kvm guest ptimer
10 45 0 0 0 GICv3 29 Level kvm guest vtimer
11 256 0 0 432 GICv3 30 Level arch_timer
12 0 0 0 0 GICv3 140 Edge uart-pl011
15 0 0 0 0 GICv3 490 Level HISI0173:00
16 0 0 0 0 GICv3 482 Level HISI02A2:00
17 0 0 0 0 GICv3 25 Level arm-pmu
19 0 0 0 0 ITS-MSI 0 Edge PCIe PME
20 0 0 0 0 ITS-MSI 1 Edge aerdrv

从上表我们可以看出系统中不同中断事件的触发情况:

  • IRQ** ID(中断号)**:表示系统为每个硬件设备或虚拟设备分配的中断标识符。
  • CPU0-CPU3 列 :表示各个 CPU 核上处理该中断的次数。 例如,arch_timer 中断在 CPU0 上触发了 256 次、CPU3 上触发了 432 次,而其他核几乎没有触发。
  • 控制器类型(Controller Type) :例如 GICv3ITS-MSI,代表不同类型的中断控制器(前者常见于 ARM 架构,后者用于 PCIe 设备等)。
  • 触发方式(Trigger Type)Level 表示电平触发,Edge 表示边沿触发。不同触发方式对应不同的中断响应机制。
  • 中断源 **/设备(Source)**:表示是谁产生了中断,比如定时器、串口、PCIe 设备等。

在实际大模型推理环境中,你可以通过 cat /proc/interrupts 实时观察到某些中断(例如 sq_send_trigger_irqcq_update_irq)在某几个 CPU 上触发频率极高。 这通常说明这些中断源被固定绑定在特定 CPU 核上,如果你的推理线程也恰好绑定在同一核,就会造成频繁的中断抢占,进而引起 HostBound 性能瓶颈

因此,在性能优化时,理解并分析这张表非常关键。 它能帮助你判断是否存在"中断集中于少数 CPU 核"的情况,为后续的绑核优化提供数据依据。


3. irqbalance 的执行逻辑

irqbalance 是 Linux 系统中一个非常重要的后台服务,它的主要职责是------在多核 CPU 系统中自动平衡中断的分布,防止某个 CPU 因为频繁处理中断而成为瓶颈。

我们可以这样理解: 如果把每次中断看成"系统电话",那么 irqbalance 就像一个总机调度员,负责把电话均匀分配给不同的"接线员"(CPU 核心),避免某一个人被电话打爆,而其他人却闲着。


(1)irqbalance 的工作方式

irqbalance 每隔一段时间(默认 10 秒)会自动执行以下几个步骤:

  1. 读取中断分布信息 它会扫描 /proc/interrupts 文件,统计每个中断号(IRQ ID)在各个 CPU 核上的触发次数。 比如发现 IRQ 1021 在 CPU10 上触发了 10 万次,而其他核几乎为 0,这说明中断分布不均。
  2. 分析负载与 NUMA 拓扑 irqbalance 并不会"盲目均衡",它会考虑 NUMA 拓扑结构,尽量让中断分配在距离设备最近的 CPU 节点上。 这样可以减少跨 NUMA 节点访问内存的延迟,兼顾"负载平衡"和"访问效率"。
  3. 重新分配中断绑定关系 通过修改 /proc/irq/<IRQ_ID>/smp_affinity 文件中的掩码值,实现把某个中断"绑"到其他 CPU 核上。 例如,如果 smp_affinity 的值是 0x400,表示该中断仅绑定在 CPU10(第 10 个核)上。
  4. 周期性重复评估 irqbalance 会不断循环以上步骤------分析 → 决策 → 调整,从而保持系统的中断分布动态平衡。

(2)irqbalance 的策略特点

  • 它关注的是中断负载均衡,而非 CPU 使用率。 即使某个 CPU 正在执行高负载任务,只要它的中断次数较少,irqbalance 仍可能把新的中断分配过去。
  • 对于部分"高频设备"或特殊场景(如 NPU、NVMe SSD),这种自动均衡反而可能带来性能抖动。 因为中断被频繁迁移,会导致 CPU Cache 失效、NUMA 远程访问增加,甚至打断关键计算线程。
  • irqbalance 的评估周期可调节,通过修改服务参数可实现更细粒度的控制,例如:
plain 复制代码
systemctl edit irqbalance
plain 复制代码
[Service]
ExecStart=
ExecStart=/usr/sbin/irqbalance --interval=300
  • 这段配置把默认 10 秒的检测周期改成了 300 秒,从而让中断分配更稳定。

(3)irqbalance 的两种典型用法

  • 通用场景:默认开启 irqbalance,让它自动维护系统中断均衡,适合普通服务器或多任务场景。
  • 性能敏感场景(如大模型推理) :手动调整 irqbalance 的策略,或者直接关闭自动调度,通过 --banirq 参数屏蔽关键中断,让开发者自行绑定中断与 CPU 核,实现更可控的性能调优。

irqbalance 的本意是"让中断更公平",但在性能调优的世界里,"公平"并不总是"高效"。 理解它的调度逻辑,有助于我们在特定场景下做出更合理的决策。 什么时候让它自动跑,什么时候让它安静下来。

(4)查看 /proc/interrupts

实时监控 IRQ 的命令

plain 复制代码
watch -n 1 cat /proc/interrupts

三、大模型推理中的中断问题

在大模型推理或训练场景中,NPU 与 CPU 间通信极为频繁。 当中断发生在业务线程所在的 CPU 核上时,推理过程会被频繁打断。Profiling 结果通常表现为:

  • Node@launch 时间增加
  • CPU 空泡率上升
  • decode 阶段耗时增长明显

如果统计 /proc/interrupts,你甚至会看到"一秒内发生两万次中断"的惊人现象。

中断频率统计代码

plain 复制代码
import time

def read_irq(irq_id):
    with open("/proc/interrupts", "r") as f:
        for line in f:
            if line.strip().startswith(str(irq_id)):
                return list(map(int, line.split()[1:5]))  # CPU0~CPU3
    return None

irq_id = 1014   # 这里填你的 sq_send_trigger_irq
interval = 0.5

prev = read_irq(irq_id)
print(f"监控 IRQ {irq_id} 触发频率... Ctrl+C 退出")

while True:
    time.sleep(interval)
    curr = read_irq(irq_id)
    diff = [c - p for c, p in zip(curr, prev)]
    print(f"{diff} 次 / {interval}s")
    prev = curr

示例运行效果如下:


四、实验验证:中断绑核前后对比

为了验证中断分布对大模型推理性能的影响,本文在 昇腾 800I(A2-A+K 架构,8 卡 * 64GB)平台 上做了一个对比实验,目标是:

通过中断绑核,观察 HostBound 现象是否得到改善。


1. 实验环境配置

组件 版本/说明
服务器硬件 Ascend 800I(A2-A+K)
操作系统 openEuler 22.03 LTS
驱动版本 25.0.rc1.1(昇腾 DevDrv 驱动)
CANN(Compute Architecture for Neural Networks) 8.2.RC1(昇腾算子库与运行时框架)
PyTorch 2.1.0
torch-npu 2.1.0.post13.dev20250722(昇腾后端模块,实现 PyTorch→NPU 映射)
MindIE 2.1.RC1-800I-A2-py311-openeuler24.03-lts(昇腾推理引擎)
模型 Qwen2.5-7B
Python 3.11.10

2. 确定中断分布与 NPU 对应关系

首先,我们要找到每个 NPU 对应的中断号(IRQ ID)。 通过以下命令查看系统中各 NPU 注册的起始中断号:

plain 复制代码
cat /proc/interrupts | grep devdrv_load_irq | cut -d: -f1

输出:

1014 1271 1529 1785 2041 2297 2553 2809

这些数字代表了每个 NPU 模块注册的中断起始编号。

接着,通过 npu-smi info 命令可查看设备的 Bus ID 与注册顺序:

plain 复制代码
npu-smi info

输出结果中,每个 NPU 的 Bus ID 和注册顺序可以一一对应,例如:

NPU ID Bus ID 驱动注册顺序
4 0000:01:00.0 第 1 个注册
5 0000:02:00.0 第 2 个注册
6 0000:41:00.0 第 3 个注册
7 0000:42:00.0 第 4 个注册
2 0000:81:00.0 第 5 个注册
3 0000:82:00.0 第 6 个注册
0 0000:C1:00.0 第 7 个注册
1 0000:C2:00.0 第 8 个注册

通过对比可以推测,中断号 1014~1271 属于 NPU0,以此类推。


3. 查看目标中断的分布情况

例如,我们关心某个 NPU 对应的 sq_send_trigger_irqcq_update_irq 中断,可在 /proc/interrupts 中查看这些中断的分布:

plain 复制代码
grep -E "sq_send_trigger_irq|cq_update_irq" /proc/interrupts

输出结果如下表:

IRQ ID CPU6 CPU7 CPU8 CPU9 CPU10 控制器 触发方式 中断源
1032 2652 1426098 0 0 195 ITS-MSI Edge sq_send_trigger_irq
1033 0 0 0 889344 0 ITS-MSI Edge cq_update_irq

从结果中可以看到,这些中断集中在 CPU7 和 CPU9 上触发非常频繁,这意味着这两个核心在推理过程中被大量中断打断。


4. 中断绑核方法

为了减少中断对推理任务的影响,我们将频繁触发的中断绑定到不执行推理线程的 CPU 核上。 操作方法如下:

  1. 关闭 irqbalance 服务(防止它自动重新分配中断):
plain 复制代码
systemctl stop irqbalance
  1. 修改中断的 smp_affinity 值
  2. 例如,将 IRQ 1032 绑定到 CPU10:
plain 复制代码
echo 0x400 > /proc/irq/1032/smp_affinity
  1. 0x400 表示第 10 个 CPU 核)
  2. 设置算子执行绑核(指定 NPU 对应的 CPU 核):
plain 复制代码
export CPU_AFFINITY_CONF=1,npu2:10,npu3:11
  1. 这样,NPU2 的算子绑定在 CPU10,而其中断(如 sq_send_trigger_irq)则绑定在其他 CPU 上,确保两者分离。

5. 实验过程与测试脚本

为了观察中断频率,我们编写了一个简单脚本模拟高频推理请求:

plain 复制代码
#!/bin/bash
LOOPS=100000
IRQ_ID="1032"

echo "开始高频推理测试,共 $LOOPS 次请求..."
for ((i=1; i<=LOOPS; i++))
do
  curl -s -X POST -d '{"model": "qwen2.5_7B", "messages": [{"role": "user","content": "请介绍一下QwQ?"}], "max_tokens": 32768, "stream": false}' http://127.0.0.1:3125/v1/chat/completions > /dev/null

  if (( $i % 100 == 0 )); then
    echo "已完成 $i 次请求,中断分布如下:"
    grep -E "^$IRQ_ID:" /proc/interrupts
    echo "-----------------------------"
  fi
done

通过该脚本,我们可以实时观察中断在各个 CPU 上的触发次数随推理进程变化的趋势。


6. 实验结果对比

场景一:算子与中断分离(未在同核)

  • 中断绑定在与算子执行线程不同的 CPU 核。
  • Profiling 结果显示:
    • Node@launch 时间稳定
    • CPU 空泡较少
    • 推理耗时正常

正常情况下的 Profiling

场景二:算子与中断共用同一 CPU 核(在同核)

  • 将中断和算子线程同时绑定在 CPU10 上。
  • Profiling 结果显示:
    • Node@launch 时间略有延长
    • CPU 空泡显著增多
    • decode 阶段耗时变长
    • 单秒中断次数高达 2 万次以上。

中断过多时的 Profiling 截图

这说明:当中断和业务线程争抢同一个核心时,即便中断处理只需微秒级时间,也会造成频繁的任务中断和缓存失效,从而让整体推理时间变长。


7. 结果分析与结论

通过对比我们可以得出:

  • 中断频率高并非主要问题,关键在于中断与计算任务是否抢占同一 CPU
  • 合理的绑核策略(让算子与中断分离)能有效减少 CPU 抢占和 cache 抖动;
  • 禁用 irqbalance 或配置 --banirq 参数,可防止系统在高负载下频繁调整中断分布。

最终结论:

中断绑核是一种低成本、高收益的性能优化手段,尤其在大模型推理场景中,可显著缓解 HostBound 问题。


五、经验建议

  1. 识别关键中断源 :重点关注 sq_send_trigger_irqcq_update_irq 等高频 NPU 中断。
  2. 业务与中断分核执行:确保业务线程与频繁中断不在同一 CPU 核。
  3. NUMA 一致性优先:绑核时尽量保持任务与中断在同一 NUMA 节点内,减少跨节点访问延迟。
  4. irqbalance 可调节而非一刀切
    1. 可通过 --banirq 精准屏蔽特定中断;
    2. 或延长评估周期,让其更"稳定"地运行。

六、结语

这次 HostBound 调优让可以让我们认识到: 中断并非只是底层系统事件,它对高性能计算任务的干扰可能远超想象。

对于大模型推理、训练这类 CPU 与 NPU 高度耦合的场景,理解并合理利用中断绑核,是性能优化中不可忽视的一环。

当性能陷入瓶颈时,不妨看一眼 /proc/interrupts ------ 也许真正打断你的,不是程序本身,而是那些"悄无声息的中断"。

注明:昇腾PAE案例库对本文写作亦有帮助。

相关推荐
七月丶2 小时前
实战复盘:我为什么把 TypeScript 写的 CLI 工具用 Rust 重写了一遍?
前端·后端·rust
王中阳Go2 小时前
05 Go Eino AI应用开发实战 | Docker 部署指南
人工智能·后端·go
普通网友2 小时前
Bash语言的图算法
开发语言·后端·golang
雨岚霏2 小时前
Bash语言的数据库编程
开发语言·后端·golang
间彧2 小时前
Java大厂面试:携程三轮面试
后端
幌才_loong2 小时前
.NET8 Middleware 核心原理与实战指南
后端
程序员飞哥2 小时前
这样做的幂等也太全了吧
java·后端·spring
百度Geek说2 小时前
百度一站式全业务智能结算中台
后端
一线大码2 小时前
安全保护协议 SSL 和 TLS 的区别
后端·http