news 2026/8/13 19:53:31

【Bug已解决】[Performance] QMoECPU<MLFloat16> intermittently livelocks (100% CPU, never returns) under m…

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Bug已解决】[Performance] QMoECPU<MLFloat16> intermittently livelocks (100% CPU, never returns) under m…

【Bug已解决】[Performance] QMoECPU intermittently livelocks (100% CPU, never returns) under multi-threaded intra-op execution on Apple Silicon 解决方案

一、现象长什么样

在 Apple Silicon(M 系列)上用 ONNX Runtime CPU EP 跑一个 QMoE(量化 MoE)模型,专家权重量化类型是MLFloat16(fp16)。开启多线程 intra-op 并行(默认就开)时,推理偶尔永远不返回,CPU 占满 100%,进程卡死:

import onnxruntime as ort so = ort.SessionOptions() so.intra_op_num_threads = 8 # 多线程 intra-op sess = ort.InferenceSession("qmoe_cpu_fp16.onnx", so, providers=["CPUExecutionProvider"]) out = sess.run(None, feed) # 偶发永远卡住,CPU 100%

最小信号:

单线程(intra_op_num_threads=1)-> 正常返回 多线程 + fp16 QMoE -> 偶发 livelock,100% CPU,永不返回 重启/换输入有时能过,不稳定

注意:这是偶发死锁/活锁(livelock),不是崩溃、不是结果错。CPU 全忙却不出结果,典型是线程同步问题。

二、背景

QMoE 的 CPU 内核为了利用多核,会把专家权重的反量化 + 矩阵乘按输出行/块拆分到多个 intra-op 线程并行做。线程之间需要同步(比如一个 barrier,等所有线程算完某一块再继续)。

在 Apple Silicon 的 ARM 架构上,线程调度和内存模型有特点:核心数多、大小核异构、缓存一致性协议对内存屏障(memory barrier)敏感。QMoE fp16 内核如果用了一个自旋等待(spin-wait)+ 普通变量来实现 barrier(即某个线程在while (!ready) {}里空转等别的线程把ready置 1),在 ARM 上极易出问题:

  • 如果ready的写没有正确的释放语义(release)、读没有获取语义(acquire),某个线程可能永远看不到ready变成 1,于是一直空转 —— 这就是 livelock(大家都在转,但永远等不到对方)。
  • Apple Silicon 的大小核调度可能把一个线程放到小核、另一个放大核,调度延迟 + 缺少屏障,让“等待方永远看不到更新”的概率变大,于是偶发。

三、根因

根因是QMoE CPU fp16 内核的线程 barrier 用了无正确内存屏障的自旋等待,在 Apple Silicon 的多线程调度下偶发永远看不到对方线程的就绪信号,导致活锁(100% CPU 空转、永不返回)

  1. 自旋 barrier 缺内存屏障ready标志的写/读没有std::atomicmemory_order_release/memory_order_acquire,ARM 弱内存模型下更新可能对等待线程不可见。
  2. 偶发性来自调度:Apple Silicon 大小核 + 调度延迟让“不可见”窗口被放大,于是多线程时才偶发;单线程没有 barrier,自然不触发。
  3. fp16 路径特有的内核:问题在内核的 fp16 变体(QMoECPU<MLFloat16>),说明 fp32 变体的 barrier 实现没问题(或用了不同同步),进一步指向是该内核特有的同步写法。
  4. 不是结果错:线程都在忙等,永远不前进,所以 CPU 100% 且不出结果。

所以这不是数值错,而是ARM 上多线程同步缺少内存屏障导致的活锁

四、最小可运行复现

下面用 C++ 标准库模拟“自旋 barrier 缺内存屏障导致活锁”(在 ARM 上极易复现;x86 强内存模型下偶发):

#include <atomic> #include <thread> #include <iostream> // 有 bug 的 barrier:用普通 bool + 自旋,无内存屏障语义 bool g_ready = false; // 应为 std::atomic<bool> 且用 acquire/release void worker() { // 等待主线程把 g_ready 置 true(自旋) while (!g_ready) { // 弱内存模型下可能永远看不到更新 -> 活锁 // 空转 } std::cout << "worker 看到 ready\n"; } int main() { std::thread t(worker); // 主线程置 ready(无 release 语义) g_ready = true; t.join(); return 0; }

在 Apple Silicon 上用clang++ -O2编译,这个程序经常会永远卡住(worker 自旋看不到g_ready更新)。把g_ready改成std::atomic<bool>并用store(true, release)/load(acquire)就立刻正常。这复现了“自旋 barrier 缺屏障 -> 活锁”。

五、解决方案(第一层:最小直接修复)

最小修复:把 QMoE fp16 内核的 barrier 换成正确的std::atomic+ acquire/release 语义,或改用条件变量/OS 提供的 barrier,不再裸自旋。对使用者,临时规避是关掉多线程 intra-op(牺牲并行换稳定):

import onnxruntime as ort so = ort.SessionOptions() so.intra_op_num_threads = 1 # 单线程,避开多线程 barrier 活锁 sess = ort.InferenceSession("qmoe_cpu_fp16.onnx", so, providers=["CPUExecutionProvider"])

对 ORT 仓库侧,修复是改 QMoE CPU fp16 内核的同步代码:

// 修复:用 atomic + 正确内存序 std::atomic<bool> g_ready{false}; // worker: while (!g_ready.load(std::memory_order_acquire)) { std::this_thread::yield(); // 让出 CPU,别空转 100% } // 主线程: g_ready.store(true, std::memory_order_release);

这一层立刻消除活锁,且yield()避免 100% 空转。

六、解决方案(第二层:结构性改进)

把“多线程内核的同步必须用正确的内存屏障、不可裸自旋”收口成唯一的配置对象OrtQmoeCpuFp16LivelockPolicy,内核实现与评审读它:

from dataclasses import dataclass, field from typing import Tuple, Literal @dataclass(frozen=True) class OrtQmoeCpuFp16LivelockPolicy: """QMoE CPU fp16 多线程同步的单一事实来源。""" # barrier 必须用带内存屏障的原子或 OS 原语,禁止裸自旋 barrier_kind: Literal["atomic_acq_rel", "condition_variable", "os_barrier"] = "atomic_acq_rel" # 自旋等待必须让出 CPU(yield),不可 100% 空转 spin_must_yield: bool = True # 必须覆盖的平台(Apple Silicon ARM 弱内存模型最易触发) must_fix_platforms: Tuple[str, ...] = ("apple-silicon", "arm", "arm64") # 受影响内核 affected_kernels: Tuple[str, ...] = ("QMoECPU<MLFloat16>",) def describe(self) -> str: return "QMoE fp16 多线程 barrier 用 atomic acquire/release,自旋必须 yield" POLICY = OrtQmoeCpuFp16LivelockPolicy() def plan_barrier(policy: OrtQmoeCpuFp16LivelockPolicy = POLICY) -> dict: return { "kind": policy.barrier_kind, "yield": policy.spin_must_yield, "platforms": policy.must_fix_platforms, }

所有多线程内核读同一份POLICY,同步写法被固化,ARM 上不再活锁。

七、解决方案(第三层:断言 / CI 守护)

把“同步用正确内存屏障、不裸自旋”做成断言。下面用 pytest 风格守护:

import pytest def test_barrier_uses_memory_barrier(policy): assert policy.barrier_kind in ("atomic_acq_rel", "condition_variable", "os_barrier") assert policy.barrier_kind != "raw_spin" def test_spin_must_yield(policy): assert policy.spin_must_yield is True def test_apple_silicon_covered(policy): assert "apple-silicon" in policy.must_fix_platforms def test_affected_kernel_listed(policy): assert "QMoECPU<MLFloat16>" in policy.affected_kernels

这四组断言锁住:(1) barrier 用正确内存屏障(非裸自旋);(2) 自旋必须 yield;(3) Apple Silicon 已覆盖;(4) 受影响内核已列入。CI 跑通即代表同步写法被守护。

八、排查清单

遇到 QMoE CPU fp16 多线程偶发卡死(CPU 100%):

  1. 先单线程验证intra_op_num_threads=1正常 -> 锁定多线程同步问题。
  2. 看是不是只 fp16 路径:fp32 正常、fp16 卡 -> 锁定该内核特有 barrier。
  3. 查内核 barrier 实现:有没有裸自旋、有没有atomicacquire/release。
  4. 临时规避:关多线程 intra-op,或暂时用 fp32 QMoE。
  5. 根本修复:barrier 改用std::atomicacquire/release(或条件变量),自旋yield()
  6. 统一策略对象:用OrtQmoeCpuFp16LivelockPolicy固化。
  7. CI 守护:断言同步用正确屏障、自旋 yield、平台覆盖。

九、小结

[Performance] QMoECPU<MLFloat16> intermittently livelocks under multi-threaded intra-op execution on Apple Silicon的根因是:QMoE CPU fp16 内核的线程 barrier 用了无正确内存屏障的裸自旋等待,在 Apple Silicon 的 ARM 弱内存模型 + 大小核调度下,等待线程偶发永远看不到对方的就绪信号,于是所有线程空转(100% CPU)却永不前进,形成活锁。

最小修复是把 barrier 换成std::atomic的 acquire/release 语义(或条件变量),自旋时yield()避免空转;临时规避是关掉多线程 intra-op;结构性改进是用唯一的OrtQmoeCpuFp16LivelockPolicy固化同步写法;CI 用四组断言守护“barrier 用正确屏障、自旋 yield、平台覆盖、内核列入”。记住:ARM 弱内存模型下,跨线程标志必须 atomic + 正确内存序,裸自旋必偶发活锁。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 19:51:40

HTTPS协议核心机制与实战部署指南

1. HTTPS协议的本质与演进历程 HTTPS&#xff08;Hypertext Transfer Protocol Secure&#xff09;本质上是在HTTP协议基础上引入安全层的加密传输方案。这个看似简单的定义背后&#xff0c;隐藏着互联网二十多年的安全演进史。早期HTTP协议以明文方式传输数据&#xff0c;就像…

作者头像 李华