先看结论与判断条件

  • 并发设计先确认解释器、delegate、输入输出缓冲区和业务上下文各自由谁拥有,不能从 API 可调用推断线程安全。
  • 单会话串行队列是缺少可靠并发证据时最容易审计的基线,它牺牲并行度换取稳定的状态边界。
  • 会话池必须让每个会话独占运行时对象和可变缓冲区,池大小由真实设备证据决定,不从 CPU 核数直接推导。
  • 取消只改变任务状态,不能在底层运行时仍使用缓冲区时提前复用或释放内存;超时也不等于执行已经停止。
  • 关闭流程先拒绝新请求,再处理排队任务、等待在途执行并释放会话;任何阶段都需要可重复且可观察。
  • 发布结论只覆盖列出的运行时版本、delegate、模型、设备和负载,单一设备通过不能外推到全部平台组合。

先把并发问题拆成所有权问题

端侧推理的竞态通常不是一个抽象的线程问题,而是两个请求在同一时间修改了同一份可变状态。解释器可能保存张量绑定和执行计划,delegate 可能持有设备资源,输入输出缓冲区会被写入,取消与关闭还会改变生命周期。若这些对象的所有权没有写清,偶发错结果、崩溃、卡死和关闭后访问就会混在一起,日志只能看到最后一个表象。可执行的起点是为每个共享对象指定唯一所有者、允许的访问线程、释放条件和失败动作。

工程上应先建立保守基线:一个运行时会话由一个执行线程拥有,每个任务创建或租用独立输入输出缓冲区,调用方只能提交不可变请求并接收 Future 或回调。该基线并不宣称所有运行时都不支持并发,而是拒绝在证据缺失时把线程安全当作默认能力。后续若要并行,应通过增加隔离会话扩展,而不是直接让更多线程进入同一对象。这样出现问题时,责任范围仍能落到单一任务、单一会话和单一缓冲区。

LiteRT、Core ML 与 ONNX Runtime Mobile 的平台和执行器模型并不完全相同,delegate 还可能只接管计算图的一部分。文章因此不提供跨框架通用的线程安全承诺,也不使用未经核实的吞吐数字。团队应把模型摘要、运行时版本、delegate 类型、设备条件、队列策略和测试回执绑定在同一记录中。只有目标组合在受控压力、取消和关闭路径上取得真实证据,才可以把结论从串行基线扩展到会话池。

端侧推理对象的所有权边界
对象推荐所有者允许共享的条件典型失败
运行时会话单个执行线程或池中单槽官方约束和项目压力回归均明确内部状态交叉或关闭竞态
delegate与创建它的会话同生命周期实现明确支持且资源隔离已验证图分区状态或设备资源争用
输入缓冲区单个任务只读且形状、类型和容量固定后一个请求覆盖前一个请求
输出缓冲区单个任务直到结果复制完成结果已经转成不可变对象读取到部分写入或旧结果
取消状态任务控制器使用原子状态并由执行线程确认调用方认为取消但底层仍在运行
关闭状态队列或会话池管理器严格执行先拒绝、再排空、后释放新请求进入已释放资源

运行时和 delegate 需要分别建账

LiteRT Android Java API 描述了在 Android 端装载模型并选择执行路径的接口,实际项目仍要检查 delegate 是否可用,并保留适用的 CPU 路径。并发契约不能只写成“解释器可用”,还要记录哪个 delegate 被请求、最终是否参与、会话在哪个线程创建、初始化失败怎样回退。delegate 创建成功只说明初始化阶段没有立即报错,不能证明多请求下输出一致,也不能证明整张图都由同一执行器完成。

TfLiteDelegate API 把 Prepare 作为图分区和 kernel 替换的重要阶段,资源生命周期则由具体实现承担。这个事实对并发设计的直接影响是:delegate 不应被当成无状态配置值随意跨会话借用。若一个会话池需要多个槽,默认做法是每个槽独立创建运行时与 delegate,并在该槽没有在途任务时释放。是否允许共享设备上下文,需要目标实现的明确约束和项目回归,不能从底层结构体存在复制指针的能力推断。

LiteRT delegate architecture 还说明未支持节点可能留在主图,执行期间会出现图分区和执行器之间的数据传递。这使错误定位不能停留在“GPU 请求”或“CPU 回退”标签上。竞态可能发生在会话输入、分区边界、临时张量或结果复制阶段。公开文章只能给出审计方法:记录执行路径、错误类别和任务身份,但不记录用户输入或模型输出;真实的并发上限、耗时和内存水位必须由候选模型与目标设备测量。

  • 运行时版本和模型摘要进入同一测试记录
  • 每个会话的 delegate 类型与初始化结果可追踪
  • 回退路径不会与主路径共享可变缓冲区
  • delegate 释放只发生在在途任务归零之后
  • 日志只保存阶段、任务号和错误类别
  • 任何并行结论都限定到已验证的实现组合

串行队列与会话池解决不同问题

串行队列的价值不是追求更高吞吐,而是把一个会话的所有可变状态收敛到单一执行上下文。请求进入有界队列后,工作线程按顺序准备输入、调用运行时、复制结果并完成 Future。调用方不能直接取得解释器或底层缓冲区,也不能绕过队列执行。这样可以把排队时间、执行时间和结果交付分开观察,同时避免两个请求同时修改张量。对交互式功能而言,还应在入队前决定队列满时是拒绝、合并还是替换旧请求。

会话池用于证明过的并行需求。池中每个槽必须拥有独立会话、delegate 和工作缓冲区,调度器只分配空闲槽,不把一个槽同时借给两个任务。池并不自动带来收益:模型权重可能重复占用内存,多个 delegate 可能争用同一硬件队列,热管理也会改变持续负载表现。池大小应由代表性设备上的内存、错误、尾延迟和业务优先级共同决定,本文不会给出脱离候选的固定数值。

两种方案都需要背压。无界队列会把突发请求转换为内存增长和过期结果,会话池若继续接受无限任务也只是把等待移到别处。业务应为每类请求明确新鲜度:相机连续帧可以丢弃旧任务,用户明确点击的单次操作可以返回繁忙并允许重试,后台批处理则可持久化进度。调度策略必须在调用方看到确定结果,不能静默丢请求,也不能把超时后的旧结果覆盖到新的界面状态。

串行队列与会话池的选择条件
决策项单会话串行队列隔离会话池放行证据
状态隔离由单线程所有权提供由独立槽位提供压力测试无交叉结果
内存成本通常只保留一份会话状态可能复制会话和设备资源真实设备峰值与退出回执
吞吐目标适合可排队或可合并请求适合确有并行价值的任务同候选对照而非理论核数
关闭复杂度排空一个执行线程逐槽停止并回收关闭期间拒绝新任务
故障隔离单次故障可能阻塞后续任务故障可限制在单槽超时、取消和重建演练
默认选择证据不足时采用获得目标实现证据后采用明确版本、设备和 delegate

生命周期必须覆盖接受、执行和关闭

一个可靠的推理服务至少有 accepting、draining 和 closed 三类状态。accepting 允许新任务进入有界队列;draining 拒绝新任务,但让已接受任务按既定策略完成或取消;closed 表示工作线程已经退出,会话和 delegate 已释放。状态转换应由单一管理器完成,并且关闭动作可以重复调用而不产生第二次释放。应用若在页面销毁、账号退出和进程后台切换时使用不同关闭语义,也要把触发原因写进调用契约。

最危险的实现是在调用方超时后立即复用输入缓冲区。Future 超时只表示等待方不再等待,不能说明底层推理已经停止;即使调用方请求取消,运行时也可能要到安全点才返回。缓冲区必须由执行线程持有到实际调用结束,结果复制完成后才可释放。若底层实现没有可验证的中断语义,取消应标记为“结果不再交付”,而不是强行终止并提前释放资源。

关闭还要处理排队任务。立即关闭适合敏感上下文失效的场景,此时未开始的任务应以明确错误完成,在途任务等待安全结束;优雅关闭适合版本切换或普通页面离开,可先停止接收再排空。无论采用哪种策略,都要保证每个已接受任务最终进入成功、取消或失败之一,不能留下永不完成的 Future。运行时释放失败需要记录并隔离槽位,不能让后续任务误用状态不明的会话。

推理队列生命周期状态机
状态是否接收新任务已有任务处理资源动作
accepting允许但受容量限制正常排队和执行会话保持可用
draining拒绝完成或按策略取消等待在途任务归零
closing拒绝所有 Future 获得终态停止工作线程并关闭会话
closed拒绝不存在排队与在途任务重复关闭不再释放
failed拒绝剩余任务返回明确错误隔离会话并等待受控重建

缓冲区隔离要覆盖输入、输出和结果交付

每个任务的输入应在入队前固定形状、类型和长度,并复制到该任务独有的不可变载荷或私有缓冲区。直接把相机、音频或 UI 层正在复用的 ByteBuffer 传给后台推理,会让生产者在消费者读取期间覆盖内容。零拷贝并非禁止,但需要更严格的租约:生产者交出所有权后不能写入,执行线程释放租约后才能复用。若项目无法证明这个协议,复制一次通常比追查偶发错结果更可控。

输出也不能在运行时写入期间暴露给调用方。工作线程应在推理返回后检查输出结构,再复制或转换为不可变业务结果,然后才完成 Future。结果对象还应带任务标识、模型版本和调用上下文版本,界面层收到结果时可以判断它是否仍然新鲜。相机帧、搜索输入和连续编辑场景尤其需要这一步,否则较慢的旧任务可能在新任务之后返回并覆盖当前状态。

缓冲区容量错误必须在进入运行时之前失败。代码需要根据公开的输入契约检查长度、维度和数据类型,不允许截断、隐式扩容或沿用上一次任务的尾部数据。输出结构不符合约定时也应返回分类错误,不能继续按旧标签表解释。本文只讨论并发所有权,输入张量契约和后处理版本仍应由各自发布门禁管理;将多个问题拆开,才能知道失败发生在并发、模型契约还是业务解释层。

  • 入队后调用方不能再修改任务输入
  • 每个任务拥有独立的输出暂存区
  • 长度、类型与形状在运行时调用前验证
  • 结果复制完成后才释放底层缓冲区
  • 结果绑定任务号、模型版本和上下文版本
  • 过期结果不会覆盖当前业务状态

背压、取消和超时必须给调用方确定语义

有界队列容量不是性能宣传数字,而是产品决策。容量过小会让用户频繁看到繁忙,容量过大会保留大量过期输入并推高内存。团队应先按请求类型定义策略,再用真实交互和设备压力校准。连续帧适合只保留最新任务,文本生成或识别按钮适合拒绝重复点击,必须逐项完成的离线任务则适合持久调度。队列层只执行已声明策略,不能自行猜测哪个请求可以丢弃。

取消应区分 queued 和 running。排队任务尚未取得会话,可以从队列逻辑上标记取消并在取出时跳过;运行任务已经持有会话和缓冲区,取消通常只能阻止结果交付,除非目标运行时明确提供并经项目验证的安全中断。该区别需要反映在 API 返回值和日志中。调用方收到 cancelled 只说明任务结果不会继续参与业务,不应据此假设底层设备资源已经立即释放。

超时是调用方等待预算,不是资源回收指令。等待超时后,管理器仍要追踪底层任务直到安全终态,并在完成时丢弃过期结果或记录错误。若大量任务持续超时,正确动作是触发容量保护、停止接收和会话健康检查,而不是继续堆积重试。重试还必须创建新的任务身份,并由业务层判断是否允许重复执行,避免同一用户操作在旧任务尚未结束时产生两个可见结果。

用有界串行执行器建立可审计基线

下面的 Python 示例表达的是并发契约,不是某个移动运行时的绑定代码。Session 由工作线程独占,submit 复制输入并使用非阻塞入队,队列已满或服务关闭会直接失败。Future 取消后,工作线程不会把结果交给调用方,但仍会让已经开始的 session.run 安全返回。每个任务都创建独立 bytes 对象,示例没有共享可写输入输出缓冲区,也不包含模型、客户数据或真实服务地址。

Session.run 在示例中代表 LiteRT、Core ML 或 ONNX Runtime 的项目适配层。适配层必须负责输入契约检查、运行时调用和结果复制,且不能把内部可变张量直接返回。close 的执行顺序是先改变 accepting 状态,再放入停止标记并等待工作线程退出,最后由工作线程关闭会话。若真实运行时要求在创建线程释放,适配层也应在同一所有权线程完成,而不是由页面线程抢先销毁。

这段代码故意在容量参数、空输入、队列满和关闭后提交时产生可达失败路径,便于测试系统验证背压而不是静默吞掉请求。生产实现还需要任务优先级、埋点脱敏、会话重建和平台生命周期集成,但不应破坏核心不变量:一个会话同一时刻只服务一个任务,输入输出归单个任务所有,关闭后不再接受请求,每个已接受任务最终都有确定状态。

有界串行推理队列与独立任务缓冲区
from concurrent.futures import Future
from dataclasses import dataclass
from queue import Full, Queue
from threading import Lock, Thread
import sys

if len(sys.argv) != 2:
    raise SystemExit("usage: inference_queue.py capacity")
capacity = int(sys.argv[1])
if capacity < 1:
    raise SystemExit("capacity must be positive")

@dataclass(frozen=True)
class Task:
    payload: bytes
    future: Future

class InferenceQueue:
    def __init__(self, session):
        self._session = session
        self._queue = Queue(maxsize=capacity)
        self._lock = Lock()
        self._accepting = True
        self._sentinel = object()
        self._worker = Thread(target=self._run, daemon=True)
        self._worker.start()

    def submit(self, payload: bytes) -> Future:
        if not payload:
            raise ValueError("payload must not be empty")
        future = Future()
        with self._lock:
            if not self._accepting:
                raise RuntimeError("inference queue is closed")
            try:
                self._queue.put_nowait(Task(bytes(payload), future))
            except Full as exc:
                raise RuntimeError("inference queue is full") from exc
        return future

    def _run(self):
        while True:
            task = self._queue.get()
            try:
                if task is self._sentinel:
                    return
                if task.future.set_running_or_notify_cancel():
                    result = bytes(self._session.run(task.payload))
                    task.future.set_result(result)
            except Exception as exc:
                if task is not self._sentinel and not task.future.done():
                    task.future.set_exception(exc)
            finally:
                self._queue.task_done()

    def close(self):
        with self._lock:
            if not self._accepting:
                return
            self._accepting = False
            self._queue.put(self._sentinel)
        self._worker.join()
        self._session.close()

发布验收要覆盖压力、取消和关闭交叉路径

Android instrumented tests 适合验证依赖真实运行时、组件和系统 API 的行为,但单一设备通过不能代表完整平台矩阵。并发验收应至少覆盖队列满、排队取消、执行中取消、页面销毁、账号退出、模型版本切换、delegate 初始化失败和会话关闭后提交。每条用例都要记录候选身份、设备、系统版本、运行时、delegate、任务序列与明确断言,不记录原始用户输入和模型输出。

正确性断言不能只看没有崩溃。受控输入需要验证结果归属,确认任务 A 的输出不会进入任务 B;取消任务不得交付结果;关闭后新请求必须失败;队列排空后所有 Future 都有终态;会话池模式还要确认一个槽位不会同时租给两个任务。性能数据应与正确性分开归档,测量条件不一致时拒绝比较。没有真实候选和设备回执,文章不声称吞吐提高、延迟降低或内存稳定。

落地前先固定一份并发契约:运行时与 delegate 组合、队列或池策略、缓冲区所有权、取消语义、关闭顺序、测试矩阵和不可外推的边界。可同时参阅本站关于模型缓存版本绑定与输出日志隐私的技术说明,避免并发治理扩写成另一套版本或日志答案。需要评估具体 App 时,可通过御盾中央平台提交脱敏模型清单、运行时组合和并发状态机,先确认测试范围,再决定是否扩大并行度。

  • 队列满时调用方获得明确失败或替换结果
  • 排队取消和执行中取消具有不同回执
  • 关闭后提交稳定失败且不会创建新会话
  • 所有已接受任务最终完成、取消或失败
  • 受控输入能够发现跨任务结果错配
  • 结论绑定模型、运行时、delegate、设备和版本

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
LiteRT Android 端需要按目标设备条件选择可用执行路径,并为不适用的 delegate 保留处理方案。LiteRT Android Java API 描述 Android 端模型装载与 delegate 使用入口。接口可用不证明目标模型在具体设备上输出一致、并发安全或获得性能收益。
delegate 的 Prepare 阶段参与图分区和 delegate kernel 替换,资源生命周期由具体实现承担。TfLiteDelegate API 描述 delegate 结构、Prepare 回调和实现责任。底层接口不承诺所有厂商实现具有相同线程模型、错误恢复或共享能力。
delegate 可能只接管部分节点,未支持节点会留在主图并形成执行器边界。LiteRT delegate architecture 说明 delegate 分区、替换和未支持节点处理。架构说明不能推出某台设备上的实际分区、数据传递成本或并发上限。
Core ML 覆盖模型集成、编译、加载与预测等平台流程。Apple Core ML 给出 Core ML 的模型集成与运行职责。平台具备预测 API 不等于同一模型实例可被任意线程并发调用,也不证明业务结果正确。
ONNX Runtime Mobile 面向 Android 与 iOS 运行模型,并受模型算子和执行提供程序约束。ONNX Runtime Mobile 描述移动端运行时、模型格式和算子支持边界。移动端支持说明不提供特定会话、执行提供程序或设备组合的并发安全结论。
依赖真实 Android 运行时和系统 API 的语义需要通过设备端测试验证。Android instrumented tests 描述在真实或模拟 Android 设备上运行测试的适用范围。单个设备和单次通过不能覆盖全部 API、ABI、厂商、delegate 与持续负载。
证据不足时先让单一线程拥有一个会话,是比无约束共享更容易审计的并发基线。工程判断:唯一所有者、独立缓冲区和有界队列能明确任务状态与资源释放责任。该基线不证明吞吐最优;是否扩展会话池必须由目标候选和设备回执决定。
取消和超时不能自动证明底层推理已经停止,因此缓冲区要等到实际执行安全结束后才能复用。工程判断:调用方等待状态与运行时资源生命周期属于两个控制面,应通过执行线程确认终态。若具体运行时提供明确且已验证的安全中断能力,项目可以采用更细的取消策略。

工程常见问题

同一个推理会话能否直接由多个线程同时调用?

不能把可调用推断为线程安全。先查目标运行时和 delegate 的明确约束,再用同一模型、设备与版本做压力回归;证据不足时采用单会话串行队列。

串行队列是否一定会让端侧 AI 变慢?

不能脱离请求类型和设备下结论。串行队列主要解决状态所有权与背压,真实体验还取决于模型、delegate、请求新鲜度和排队策略,应在正确性通过后单独测量。

会话池大小是否可以直接设置为 CPU 核数?

不建议。会话会占用模型状态、缓冲区和设备资源,delegate 也可能共享硬件队列。池大小应由目标设备的内存、错误和持续负载证据决定。

Future 超时后能否立即复用输入缓冲区?

不能。超时只代表等待方停止等待,底层推理可能仍在读取输入。应由执行线程在实际调用返回后释放租约,再允许缓冲区复用。

页面销毁时应该立即关闭推理会话吗?

取决于会话归属。页面专属会话可进入拒绝新请求和排空流程;进程级共享会话不应被单个页面直接销毁。无论哪种情况,都要让已接受任务获得确定终态。

准备并发推理评估时需要提供哪些材料?

准备脱敏模型清单、运行时版本、delegate 选择、队列或池策略、输入输出契约、取消与关闭状态机、代表设备范围和受控测试用例,再固定候选身份。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 移动 AI 应用的运行时安全边界