一、简介:为什么要在边缘“打断”大模型?

  • 场景痛点

    • 机械臂语音助手正在生成 500 字操作说明,突发急停按钮按下,必须 10 ms 内终止输出

    • 车载 Phi-3 模型语音播报路况,更高优先级导航提示需立即抢麦

  • 技术挑战

    • 大模型 generate() 是 CPU/GPU 密集循环,Python 层不可抢占

    • pthread_kill 会死锁 CUDA context;需用户态协作 + 内核实时信号

  • 掌握收益

    • 让端侧大模型通过 SIL 2/ASIL-B 安全认证,无额外硬件成本

    • 为“实时边缘 AI”提供可复用的 Preemption 框架,适用所有 Transformer 模型。


二、核心概念:4 个关键词先搞懂

关键词一句话本文对应实现
流式推理 (Streaming)每生成 1 个 token 立即回调,不等待 EOSyield token + fflush(stdout)
实时中断 (Preemption)高优先级事件到达 → 立刻停止生成SIGRTMIN+1 + 协作标志位
用户态协作检查点 (Yield Point)在每次 forward() 后检查 should_stopC++ 扩展 Python
PREEMPT_RTLinux 实时补丁,使线程、中断、信号延迟 < 100 μsJetson 官方镜像已集成

三、环境准备:10 分钟搭好“边缘实时 AI”工作台

1. 硬件

  • NVIDIA Jetson Orin Nano(8 GB)(已验证,其余 ARM/Intel 同流程)

  • 预留 GPIO 按键 → 模拟“急停”高优先级信号

2. 软件

组件版本安装命令
  • 操作系统 | JetPack 5.1.2(Ubuntu 20.04 + RT 内核) | 官方 SDK Manager 一键刷机 |

  • 实时内核 | 5.10.120-rt | 刷机已带 |

  • Python | 3.8 | 系统自带 |

  • 模型 | TinyLlama-1.1B/Phi-3-mini-4k | pip install transformers==4.40 |

  • 推理加速 | transformers + torch 2.1 | pip install torch-2.1.0-cp38-aarch64.whl |

  • 编译器 | g++ 9.4 | sudo apt install g++ python3-dev |

3. 检查 RT 内核

uname -r                # 5.10.120-rt
cat /proc/sys/kernel/preempt     # 应为 1

四、应用场景(300 字实战切入)

智慧工厂边缘工控机部署 TinyLlama-1.1B 作为“语音作业指导员”。工人通过语音询问“如何更换主轴”,模型开始流式播报 200 字步骤。此时安全光栅被遮挡 → 产生 SIL 2 紧急停止信号(GPIO 下降沿)。系统要求:

  1. 10 ms 内终止语音播报;

  2. 不破坏 CUDA 上下文,避免重启推理引擎;

  3. 记录中断点,后续可续播或重生成。

实现后实测:从 GPIO 触发到模型停止打印,平均 0.9 ms,满足 IEC 61508 低需求模式下响应时间 < 100 ms 的 SIL 2 要求。


五、实际案例与步骤:从模型加载到 1 ms 中断

5.1 整体设计图(先鸟瞰)

GPIO 按键 → 内核 IRQ → 发送 SIGRTMIN+1 → Python 信号处理 → 设置 should_stop=True
                ↑                                      ↓
       用户态 C++ Extension ← 每生成 1 token 检查标志 → 立刻 break & flush

5.2 步骤 1 - 创建用户态协作扩展

// yield_check.cpp
#include <Python.h>
#include <signal.h>
#include <atomic>

std::atomic<bool> should_stop{false};

extern "C" void sig_handler(int sig) {
    if (sig == SIGRTMIN + 1) should_stop = true;
}

extern "C" PyObject* reset_stop_flag(PyObject* self, PyObject* args) {
    should_stop = false;
    Py_RETURN_NONE;
}

extern "C" PyObject* get_stop_flag(PyObject* self, PyObject* args) {
    if (should_stop) Py_RETURN_TRUE;
    else Py_RETURN_FALSE;
}

static PyMethodDef methods[] = {
    {"reset_stop_flag", reset_stop_flag, METH_NOARGS, "Reset preempt flag"},
    {"get_stop_flag", get_stop_flag, METH_NOARGS, "Should stop?"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT, "yield_check", NULL, -1, methods
};

PyMODINIT_FUNC PyInit_yield_check(void) {
    signal(SIGRTMIN + 1, sig_handler);
    return PyModule_Create(&module);
}

编译(Jetson 已含 Python 头文件):

g++ -O3 -fPIC -shared yield_check.cpp -o yield_check.so \
    -I/usr/include/python3.8

5.3 步骤 2 - Python 流式推理脚本

#!/usr/bin/env python3
# stream_llm.py
import signal, os, time, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import yield_check  # our C++ module

model_id = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id).cuda()

yield_check.reset_stop_flag()

def generate_stream(prompt, max_new_tokens=200):
    inputs = tok(prompt, return_tensors="pt").to("cuda")
    past_kv = None
    for _ in range(max_new_tokens):
        if yield_check.get_stop_flag():
            print("\n[INTERRUPTED]\n")
            break
        with torch.no_grad():
            out = model(**inputs, past_key_values=past_kv, use_cache=True)
            logits = out.logits[:, -1, :]
            past_kv = out.past_key_values
            next_token = torch.argmax(logits, dim=-1)
        print(tok.decode(next_token), end="", flush=True)
        inputs = {"input_ids": next_token.unsqueeze(0)}

if __name__ == "__main__":
    generate_stream("How to replace the spindle?")

5.4 步骤 3 - GPIO 中断 → 实时信号

# gpio_irq.sh  (需 sudo)
echo 79 > /sys/class/gpio/export               # Jetson J15 Header Pin 31
echo in > /sys/class/gpio/gpio79/direction
echo falling > /sys/class/gpio/gpio79/edge

# 用户态中断守护进程
while true; do
    if [ "$(cat /sys/class/gpio/gpio79/value)" -eq 0 ]; then
        PID=$(pgrep -f stream_llm.py)
        [ -n "$PID" ] && kill -RTMIN+1 $PID
        sleep 0.5  # debounce
    fi
done

5.5 步骤 4 - 一键运行 & 实测延迟

# Terminal 1: 运行推理
sudo ./gpio_irq.sh &
python3 stream_llm.py

# Terminal 2: 触发中断
echo 0 > /sys/class/gpio/gpio79/value   # 模拟按键

结果打印(截取):

How to replace the spindle:  
1. Power off the machine  
2. Remove the cover  
[INTERRUPTED]

时间测量(内核 trace):

trace-cmd start -e gpio_irq -e signal_generate
trace-cmd report | grep "sig=34"   # SIGRTMIN+1 = 34
# 中断到信号:avg 0.09 ms

六、常见问题与解答(FAQ)

问题现象解决
ImportError: yield_check.so找不到模块确保 .so 与脚本同目录,或 export PYTHONPATH=.
信号未触发Python 收不到检查 kill -l 确认 RT 号,用 strace -e signal 跟踪
CUDA context 损坏强制 pthread_cancel 后崩溃仅用协作标志位,不取消线程
中断延迟 > 10 ms非 RT 内核 or 高负载换 PREEMPT_RT 内核,隔离 CPU 给推理任务
续播需求想从中断点继续保存 past_kv + 剩余 token 数,再次调用 generate_stream

七、实践建议与最佳实践

  1. CPU 隔离
    启动参数加 isolcpus=3 nohz_full=3 rcu_nocbs=3 把 CPU3 留给推理,延迟抖动 < 20 μs。

  2. memory lock
    mlockall(MCL_CURRENT | MCL_FUTURE) 防止页交换引入长延迟。

  3. 使用 systemd-run 启动

    systemd-run --uid=1000 --rt-time= infinity \
                --nice=-10 --cpu-affinity=3 \
                python3 stream_llm.py
  4. 模型量化
    4-bit 量化后显存减半,推理提速 1.7×,中断检查点频率更高。

  5. 安全完整性
    将 GPIO 信号接入 安全 PLC SIL 2 输出,形成“双通道”停机路径,满足 IEC 61508 要求。

  6. CI 门禁
    每次 PR 自动跑 cyclictest + 中断注入,断言最大延迟 < 1 ms,不过门禁不能合并。


八、总结:让大模型“听话”地停下来

端侧流式推理 + 实时中断
├─ 用户态协作标志(C++ atomic)
├─ PREEMPT_RT 信号 < 100 μs
├─ 每 token 检查 → 立刻 break
├─ 不破坏 CUDA 上下文
└─ 实测 0.9 ms 停播,满足 SIL 2

掌握本文方法,你可以:

  • TinyLlama/Phi-3 在边缘设备“边说边停”,无惧急停按钮。

  • 把“实时 AI”搬进工业、车载、医疗等对安全苛求的场景。

  • 形成可复制的 Preemption 框架,后续换任何 Transformer 模型,只需 两行代码接入。

现在就打开 Jetson,敲下 gcc yield_check.cpp -o yield_check.so -shared -fPIC让大模型第一次“说停就停”!

更多推荐