news 2026/9/8 18:44:34

昇思大模型推理服务生命周期管理:从状态机到优雅下线实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
昇思大模型推理服务生命周期管理:从状态机到优雅下线实践

刚开始做昇思大模型推理服务的时候,我栽得最狠的一次就是在“升级模型版本”。当时想法很简单:新模型推理代码写好,直接把服务停了,换权重,再启动。结果呢?服务中断了将近十分钟,线上积压的推理请求全部超时,下游业务报警电话被打爆。那一刻我就意识到,在大模型推理系统里,模型能跑起来只是第一步,真正难的是让整个服务在部署、扩容、升级、保活、下线这条链路上长期稳定地运转。昇思大模型推理系统的生命周期管理,说白了就是把“实例从生到死”的每个环节都定义清楚、控制到位,配套一套清晰的架构和可靠的代码逻辑。

这篇文章我不会只讲概念,会把我在昇思推理服务上实际用的流程拆解、架构分层、核心状态机代码和线上踩坑记录都摆出来。适合正在做模型推理平台、部署大模型服务,或者想把现有推理服务做得更稳的工程师阅读。内容会有一点密度,但可以直接拿去做参考。

1. 昇思大模型推理系统生命周期管理管什么:先理清边界

1.1 从模型发布到实例回收,生命周期有哪几个阶段

我习惯把昇思大模型推理系统中的一个服务实例,看作一个独立的“推理工作进程”。在分布式架构里,这类进程不会只启动一次就一直活着。它要经历构建、注册、调度、运行、更新、下线等多个阶段,而且每个阶段都不能跳过。具体可以分为:

  • 构建阶段:从模型仓库拉取昇思模型文件,加载权重,初始化昇思MindSpore执行上下文,编译计算图。这个过程可能耗时几十秒到几分钟不等。
  • 注册阶段:实例启动后把自己注册到控制面,上报IP、端口、模型版本、显存占用、负载能力等信息,让调度器知道“这个实例可以接流量了”。
  • 运行阶段:实例对外提供推理服务,包括请求接收、预处理、模型推理、后处理、结果返回,并持续上报心跳和监控指标。
  • 更新升级阶段:模型版本迭代或推理逻辑变更,需要对新实例进行预热、灰度发布、流量切换,确认稳定后再回收旧实例。
  • 下线回收阶段:实例因缩容、故障、升级完毕等原因被回收。需要先摘除流量,再等待在途请求结束,最后释放显存和临时文件。

只跑通“运行阶段”不算生命周期管理。昇思推理系统的生命周期管理,核心在于把这几个阶段之间的状态转换做成可观测、可控制、可回滚的流程。比如“构建完成”不代表“可以接收流量”,必须显式进入“就绪”状态才算数。同理,“缩容”不代表直接kill进程,而是先排空在途请求,再优雅退出。边界先定清楚,后面所有代码逻辑才有依据。

1.2 大模型推理场景下的管理难点:不是Web应用那种生命周期

做过普通微服务开发的同事经常会有一套现成的生命周期管理思路。服务启动时注册到注册中心,调用健康检查接口判断存活,下线时反注册,Kubernetes里靠探针搞就绪和存活。这套思路搬到昇思大模型推理系统里,一开始好像也说得通,但实际跑下来会发现有几个明显差异。

第一,大模型实例启动极其昂贵。加载一个动辄几十GB的权重文件到显存,还要做图编译和算子优化,冷启动阶段通常要几十秒,甚至几分钟。如果就绪探针判断得太粗,很可能实例还没真正准备好,Scheduler就把请求分给它了,结果就是大量请求排队等待,拖垮整个Batch调度。第二,大模型推理是有状态的。这里的“状态”不是业务状态,而是显存里的KV Cache、权重参数、推理引擎上下文。这些状态不能像普通无状态服务那样随便复制和销毁。缩容任何一个实例,都可能把一部分已经缓存的模型上下文丢掉,导致后续请求又要重新做prefill,时延瞬间飙升。第三,扩容要受物理资源硬约束。昇思推理服务通常跑在GPU或昇腾AI处理器上,单个实例占用一整块或大半块设备显存。扩容不是多拉几个容器那么简单,需要先在设备上腾出显存、加载权重,这和普通Web容器“秒级拉起”完全不是一个量级。

所以昇思推理系统的生命周期管理,需要一套更重的流程:实例加载、预热、就绪确认、排空、优雅销毁,每一步都要有明确的状态和操作接口。直接套用普通服务管理方式,后面会不断出问题。

1.3 生命周期管理的关键指标和优化目标

理清生命周期阶段之后,工程上还需要落地可量化的指标。没有指标,就谈不上管理和优化。我在实际项目中主要关注三组指标:

生命周期阶段核心指标主要风险
启动预热预热耗时、加载成功率、显存分配峰值加载慢导致扩容不及时,加载失败导致部署中断
注册就绪就绪比例、心跳成功率未就绪实例被调度,请求异常堆积
运行服务QPS、推理时延、Batch利用率、设备利用率负载不均、Bad Batcher、长尾请求阻塞
扩容缩容扩缩容耗时、冷却期次数、缩容失败率扩容抖动、缩容误杀在途请求
更新发布灰度批次大小、发布回滚耗时、错误率新旧版本共存导致显存超卖
优雅下线排空耗时、残留在途请求数、释放失败率请求被硬杀、显存无法回收

这套指标不只是用来做监控大屏,更重要的是驱动调度器的控制策略。比如扩容不是因为QPS瞬时升高就立刻扩,而是结合单实例处理能力、排队长度、设备空闲情况做综合决策。在后文讲代码实现时,很多判断逻辑都会引用这里的指标维度。

2. 昇思大模型推理系统的整体架构与控制链路

2.1 逻辑架构分层:入口到模型实例,谁在管谁

昇思大模型推理系统的整体架构,我会分成五个逻辑层来说。这样聊生命周期管理时,能清楚知道每个模块到底在哪一层、扮演什么角色。

最上面是接入层,负责接收外部推理请求,做路由和负载均衡。接入层不关心具体是哪个模型实例在处理请求,只根据路由规则、权重大小把请求转发给后面的数据面实例。它是最先感知到流量变化的组件,也是生命周期切换时流量摘除的执行者。

接入层下面,是控制面。控制面是整个生命周期管理的大脑,通常包含实例注册中心、调度器、配置中心和状态管理器。它知道当前所有实例分别处于什么状态,哪个实例刚启动、哪个实例可以接流量、哪个实例准备下线。调度器会结合监控数据做扩缩容决策,并把决策下发到具体的实例池。

再往下是数据面。数据面由一组昇思推理实例组成。每个实例内部加载了对应的模型权重,提供HTTP或gRPC推理接口。实例启动后主动向控制面注册,持续上报心跳。它们才是真正执行模型推理的地方,也是生命周期管理对象的实体。

侧面还有存储面和监控面。存储面用来管理昇思模型文件、版本清单、部署配置,负责解决“版本从哪里来、被谁依赖、何时不可变”的问题。监控面则持续采集指标,比如实例的显存占用、推理时延、QPS、队列深度,并把数据喂给控制面的调度器做决策。

从控制链路来看,数据面永远是被管理方,控制面不会直接入侵式地杀死数据面进程,而是通过状态指令,比如“置为DRAINING”“触发缩容”“切换版本”,推动实例自己完成优雅动作。这样的分层能避免控制面和数据面耦合过深。刚开始我做的时候总是直接按进程管理思路去处理实例,后来重建了这套分层,才真正把生命周期从“运维命令”变成了“系统能力”。

2.2 生命周期管理模块在分布式架构中的位置

昇思推理服务实例通常不止一台设备一个实例,而是一个由几十个实例组成的实例组。在这种分布式架构下,注册中心和调度器必须和处理推理的数据面分离,否则控制链路和数据链路会互相抢占资源。一个常见的错误是让某个推理实例兼任注册中心,结果这个实例一扩缩容或升级,全系统调度就出问题。

我们可以把实例看作一个持续汇报状态的Agent,每个实例做的事情很简单:启动后上报自己的能力,运行中定期汇报健康状态,接到下线指令后配合摘流和退出。而控制面中的调度器,则更像整个系统的架构设计师,它不直接参与推理,但决定哪个实例活着、哪个实例应该退休、哪个实例需要被拉起。控制面和数据面之间通过一套明确的状态协议交互。

这套协议通常包含注册请求、心跳请求、状态变更请求、下线通知。心跳要做得细致,除了简单说“我还活着”,最好能携带当前排队长度、显存余量、最近一次推理耗时。调度器根据这些信息判断是扩容还是缩容,是保持版本稳定还是触发灰度发布。设计上要注意控制面的接口一定要幂等。比如同一个实例重复注册,注册中心要能覆盖旧记录而不是报错;同一个下线通知被发了两次,实例不能因为第二次重复通知就再走一遍销毁流程。这些细节在分布式架构里看着不起眼,实际运行中非常容易出现。

2.3 中心化调度与自注册:昇思推理场景里我为什么这么选

生命周期管理的架构路线,主要有两种极端:一是彻底的中心化调度,由控制面维护全量实例信息,调度器主动下发启动、停止命令;二是自注册模式,实例自己上报自己、自己决定退不退,控制面只做轻量记录。

我在昇思推理系统里最终采用了“中心化调度为主,自注册为辅”的混合路线。原因是昇思大模型推理实例的生命周期动作太重,比如加载一个模型需要几分钟,调度器必须有能力在全局视角下判断“当前是否值得再拉起一个实例”。如果完全靠实例自注册,缺乏全局队列和资源视图,很容易在流量高峰时出现多个实例同时冷启动,导致设备显存超分或推理Batcher资源被抢占。

自注册的“为辅”体现在实例启动后主动上报状态。实例上报的IP、版本、资源能力等信息,是注册中心的主要数据来源。调度器不会去猜某个设备上是否已经运行了实例,而是等待实例上报后再加入调度池。同时,调度器保留对实例的强制下线能力。这个能力是为了防止实例异常失去响应时,不会永远占用资源。运维上可以用一条管理命令强制回收,但平时大部分情况下都让实例走优雅下线流程。

还有一个关键点是模型和实例的解耦。昇思模型文件不建议直接放在实例本地磁盘上反复改动,而应该以版本形式集中存储。实例每次启动都从模型仓库拉取指定版本,生成不可变的模型快照。这样生命周期管理在升级时,实际上管理的是“某个版本的实例池”替换为“另一个版本的实例池”,而不是在同一个实例上做原地代码变更。这也是我能做好灰度发布的基础。

3. 核心流程逐段拆解:预热、扩容、发布、优雅下线

3.1 模型预热流程:为什么权重加载完成不等于就绪

在昇思推理系统里,“实例启动”和“实例就绪”之间隔着一整个准备阶段。很多刚开始做部署的同事,会拿Kubernetes的存活探针逻辑来套,进程起来了就认为Pod Ready,结果大量请求打到还在加载模型的实例上,直接导致推理超时。后来我改了判断标准:只有实例完成权重加载、执行图编译、算子融合、显存分配和预热推理之后,才能被判定为Ready。

具体来说,一个昇思大模型实例从进程启动到Ready,通常会经历几个阶段。第一步是进程启动,读取配置文件,建立日志和监控上下文。第二步是从模型仓库拉取对应版本的MindSpore模型权重文件。模型文件可能很大,这一步会有磁盘I/O和网络传输开销。第三步是加载模型到昇思MindSpore运行时,创建推理图,设置运行设备。这里有个容易忽略的点:如果不先安排好显存池和KV Cache空间,后续推理到一半可能OOM。所以生命周期管理中要把“显存预分配”作为一个显式阶段来处理,而不是等运行时自己按需分配。第四步是跑一次微型验证请求或冷启动推理,确认图执行链路没有报错。有的推理引擎还支持compile后导出优化图,这个过程会更久。

等上面几步全部通过,实例才应该把自己标记为Ready,然后向注册中心发送就绪信号。在架构设计上,这一步通常由控制面根据实例上报的“状态字段”判断。状态字段在Ready之前不允许被调度器选为路由目标。我在3.1最开始说的那次停机事故,就是因为预热流程没有做状态控制,进程一起来就被接入层认为是健康实例,请求打进来全卡在权重加载上。这个坑只要在生命周期管理的状态设计里堵住,后面就不会再犯。

3.2 容量决策与实例调度:扩缩容怎么触发更稳

大模型推理实例不像普通API服务,随便拉一个进程就完事。昇思推理实例每增加一个,就要额外占用加载时间、显存和设备带宽。所以扩缩容的触发条件不能只看“当前CPU到了80%”这种粗糙指标。我在线上使用的决策维度有四个:请求排队长度、单实例平均推理时延、实例池的Batch利用率、设备显存余量。

举个例子。假设当前每一台昇思推理实例的单卡极限吞吐是每秒200个推理请求,线上QPS到了400,同时排队中的请求数超过了模型单实例可承受的Batch上限。这时候调度器就可以计算出目标实例数。最朴素的计算公式是:

目标实例数 = ceil(当前QPS / 单实例预期吞吐)

但这里有一个“预期吞吐”的坑。如果单实例还在预热阶段,它的吞吐能力没有完全发挥出来,用静态标称值去估,扩容出来的实例可能在几分钟内都是空的。所以我在计算时还会把排队延迟和Batch制式的利用率一起纳入评估。排队延迟超过100ms且Batch利用率在80%以上,才认为有扩容诉求,而不是瞬时抖动就扩容。

缩容比扩容更需要克制。一个正在处理Batch推理的实例,如果因为缩容信号而被强行销毁,当前批次的几十个请求会全部失败。因此缩容前必须先让实例进入DRAINING状态,停止接收新请求,然后等待在途请求处理完毕。等待时间最好是可配置的,我一般设置为30秒。超过等待时间仍然没有处理完的请求,要记录日志后再做强制清理。强制下线不能做得太频繁,否则会导致请求成功率持续波动。

还有一点是关于冷却期的设计。调度器做了一次扩缩容之后,即使指标仍然触达阈值,也不能在几秒内连续执行第二次变更。因为实例启动需要时间,负载和容量之间是延迟关系,没有冷却期的调度器会“追尾”,刚决定扩容,容量还没上来,又连续扩容几次,等实例全部Ready后流量已经回落,资源白白浪费。冷却期通常要和实例预热时间挂钩。昇思大模型推理实例的预热时间可能是两三分钟,那冷却期就不能低于1分钟,否则很容易抖动。

3.3 版本更新与灰度发布:切流与回滚到底怎么操作

模型版本升级大概是生命周期管理里最需要谨慎对待的环节。拿昇思MindSpore Serving这种系统来说,一个服务实例背后绑定的是明确的模型版本。如果直接在旧实例上覆盖模型文件再重启,一方面要承受较长中断时间,另一方面新版如果存在问题,很难快速回滚到旧版,因为旧版的权重文件可能已经被覆盖了。

我采用的方案是把版本和实例组绑定。每个模型版本对应至少一个独立的实例组。发布的时候,并不直接改动已经在运行的旧实例组,而是先按新版本拉起一个新的实例组,等新实例组全部预热完成、报告Ready之后,再通过接入层的流量权重把请求逐步从旧实例组切到新实例组。整个过程中旧实例组始终在线,一旦新版本出现异常,只需要把流量权重切回去即可。

灰度发布时有一个我不能省掉的步骤:新版本实例组在接收真实流量前,必须跑一批模拟请求做基准校验。只跑到Ready状态还不够,因为Ready只能说明模型能推理,不代表推理结果和预期一致。昇思模型一般不会在版本号上发生变化后又提供完全一致的输出,但我们需要确认时延、显存占用这些关键指标不超过预期。我通常在灰度流程里增加一个“Canary验证”阶段,放5%的真实流量过去,观察错误率和时延分布,最后再决定是否放大到100%。

发布过程中,资源成本会短暂上升,因为新旧两套实例组同时在跑。这一点要在调度器的容量模型里预留出来。如果一个实例池的显存资源已经被旧版本占掉80%,那就不应该这时候再启动新版本实例组,否则设备显存会直接超分。我一般会在发布前检查总资源余量,确认有足够空间再进入发布流程,否则会把发布任务挂起,等缩容或资源回收完成后再继续。

3.4 优雅下线:摘流、排空与资源回收,一个都不能少

实例下线是生命周期管理里最容易被低估的一步。很多人觉得下线不就是把进程杀掉吗?但在昇思大模型推理系统里,这个动作包含了好几层逻辑。直接杀进程会让正在处理的推理请求丢失,同时显存里的模型上下文来不及清理,临时文件也可能残留,长此以往会越积越多。

优雅下线的第一步,是把实例的状态从Ready改成DRAINING。这个状态变更要同步给注册中心和接入层。接入层在拿到状态后,会停止向该实例分配新的请求,但保留已经进入实例队列的请求继续处理。第二步是给实例一个排空时间窗口,让在途请求能够自然结束。我的做法是实例在收到下线指令后启动一个计时器,计时器到期之前,自己主动对请求处理循环做屏障,确保不会再有新的子任务被拉起。等请求数归零之后,再通知调度器已经完成排空。第三步才是真正的资源释放,包括关闭昇思推理上下文、释放显存、删除临时文件、关闭监听端口。资源释放完成后,实例才真正退出。

实际中“从DRAINING到真正退出”经常卡在某个请求上。比如有一个长尾推理请求处理了很长时间,排空时间窗口已经过了,但进程还挂在等结果返回。遇到这种情况,我们不能无限等待。我的策略是:排空时间窗口结束后,强制中断在途请求并记录上下文信息,包括请求ID、模型版本、当前阶段。这样虽然会丢失极少数请求,但能保证实例不会变成一个永远退不掉的僵尸。要准确知道某个请求是否还在处理中,需要在代码里维护一个活跃请求计数器。后文的代码示例会专门展示这个逻辑。

4. 生命周期管理的代码实现:状态机、注册与优雅下线

4.1 用状态机建模实例生命周期

要把生命周期管理落在代码上,首先要定义清楚状态。为昇思大模型推理实例建模状态机时,我不建议用一堆散落的布尔变量表示“是否初始化、是否在排空”,这样很容易出现状态组合错误。用枚举加状态迁移校验会更清晰。

from enum import Enum from dataclasses import dataclass, field import threading import time import logging logger = logging.getLogger("inference-lifecycle") class InstanceState(str, Enum): INITIALIZING = "initializing" READY = "ready" DRAINING = "draining" STOPPED = "stopped" ERROR = "error" # 合法状态迁移表 ALLOWED_TRANSITIONS = { InstanceState.INITIALIZING: {InstanceState.READY, InstanceState.ERROR, InstanceState.STOPPED}, InstanceState.READY: {InstanceState.DRAINING, InstanceState.STOPPED, InstanceState.ERROR}, InstanceState.DRAINING: {InstanceState.STOPPED, InstanceState.ERROR, InstanceState.READY}, InstanceState.STOPPED: set(), InstanceState.ERROR: {InstanceState.INITIALIZING, InstanceState.STOPPED}, } @dataclass class Instance: instance_id: str model_version: str address: str state: InstanceState = InstanceState.INITIALIZING active_requests: int = 0 _lock: threading.Lock = field(default_factory=threading.Lock) def transition_to(self, target: InstanceState) -> bool: """ 状态迁移统一入口。非法迁移会被拒绝, 避免代码里随手把 DRAINING 改回 READY 造成不可预期行为。 """ with self._lock: if target not in ALLOWED_TRANSITIONS[self.state]: logger.warning( "invalid transition %s -> %s on instance %s", self.state, target, self.instance_id ) return False old_state = self.state self.state = target logger.info( "instance %s state %s -> %s", self.instance_id, old_state, target ) return True

这个状态机是所有生命周期管理逻辑的基石。注意我把DRAINING也允许切回READY,听起来有点反常,但实际有用流量异常时先摘流,如果发现实例本身没有故障,再把流量恢复,可以避免因误判导致不必要的重启。

4.2 实例注册与心跳续约逻辑

注册中心不能只靠启动时上报一次。昇思大模型推理实例在长时间运行过程中,可能因为显存分配不稳定、网络链路变化,导致实际可用性和控制面记录不一致。所以我在代码里加了心跳续约逻辑:每个实例启动后定期向注册中心上报状态和健康信息。

下面的代码是一个模拟的心跳上报线程,实际项目中会把HTTP调用替换成gRPC或服务端接口。关键在于使用独立线程,不让心跳逻辑阻塞推理主流程。

import requests import threading class InstanceHeartbeat: """ 实例心跳线程。每 10 秒上报一次状态。 如果注册中心连续多次没有收到心跳,就会把实例标记为不健康, 从而避免把请求调度到一个已经卡死的实例上。 """ def __init__(self, instance: Instance, registry_url: str): self.instance = instance self.registry_url = registry_url.rstrip("/") self._stop_event = threading.Event() def _report_once(self): payload = { "instance_id": self.instance.instance_id, "model_version": self.instance.model_version, "address": self.instance.address, "state": self.instance.state.value, "active_requests": self.instance.active_requests, "timestamp": int(time.time()), } try: response = requests.post( f"{self.registry_url}/heartbeat", json=payload, timeout=3, ) if response.status_code != 200: logger.warning("heartbeat failed: %s", response.text) except Exception as exc: logger.warning("heartbeat request error: %s", exc) def start(self): def run(): while not self._stop_event.is_set(): self._report_once() self._stop_event.wait(10) threading.Thread(target=run, daemon=True).start() def stop(self): self._stop_event.set()

这里有一个很容易被忽略的问题:active_requests字段。注册中心看到的不只是实例存活状态,还要知道它当前有多少请求在处理。当调度器想做下线决策时,这个字段非常关键。如果实例自己说active_requests已经归零,控制面就可以放心让它退;如果说还有几百个在途请求,即便进程活着,调度器也要谨慎处理,不能贸然把它从实例池里摘掉。

4.3 优雅断流与排空实现:基于并发请求计数

实例要优雅下线,必须能感知到“当前还有多少请求在处理”。我在代码里用一个活跃请求计数器来追踪。每次请求进入core推理模块时计数器加一,请求完成时减一。当实例收到DRAINING指令后,设置一个排空标志,阻止新的请求进入,然后等待计数器归零。

import time from contextlib import contextmanager class RequestTracker: """ 请求追踪器。正常接收新请求时 state = READY, 一旦进入排空流程,禁止新请求进入,同时等待活跃请求数归零。 """ def __init__(self, instance: Instance): self.instance = instance self._draining = False @contextmanager def handle_request(self, request_id: str): # 如果实例正在排空,拒绝新请求进入 if self._draining: raise RuntimeError("instance is draining, reject new request") with self.instance._lock: self.instance.active_requests += 1 try: yield finally: with self.instance._lock: self.instance.active_requests -= 1 def start_drain(self): self._draining = True self.instance.transition_to(InstanceState.DRAINING) logger.info("drain started, active_requests=%d", self.instance.active_requests) def wait_for_drain(self, timeout_seconds: int = 30) -> bool: deadline = time.time() + timeout_seconds while time.time() < deadline: with self.instance._lock: if self.instance.active_requests == 0: return True time.sleep(0.5) return False def force_stop(self): self.instance.transition_to(InstanceState.STOPPED) logger.warning("force stop after drain timeout")

这个实现的精巧之处在于contextmanager。推理主流程只要用with tracker.handle_request(...)包住,就不用关心后续的下线逻辑。等到要下线的时候,实例只需要调用start_drain和wait_for_drain,就能确保在途请求先结束。

实际线上服务里,我还在wait_for_drain返回False时把instance.active_requests的数值记录到日志。这些记录是排查“为什么下线卡死”的第一手证据。

4.4 调度器中的生命周期状态决策和配置示例

调度器要做两件事:决定目标实例数,以及决定哪些实例可以被选为路由目标。前者根据容量指标和冷却期控制,后者根据实例上报的状态过滤。没有把状态过滤做死的调度器,会在实例还在初始化时就把流量打过去,造成大量超时。

调度器维护的实例列表通常来自注册中心汇总。我给出的配置示例参考了YAML管理方式:

service: name: qwen_resnet_or_llm_serve model: mindspore-llm-v3 version: 20250601_001 replica_limits: min_replicas: 2 max_replicas: 8 scale_policy: cool_down_seconds: 60 queue_depth_threshold: 50 avg_latency_p95_threshold_ms: 300 gpu_idle_memory_threshold_gb: 20 lifecycle: prewarm_timeout_seconds: 300 drain_timeout_seconds: 30 force_kill_after_timeout: true

调度器根据这个配置生成决策。选择路由目标时,只会选择state=READY的实例。当某个实例状态是DRAINING时,即使它还在处理请求,也绝不会被新流量命中。状态过滤放在调度最前端,和容量计算解耦。这样做的好处是,无论容量算法怎么调参,都不会影响“不让未就绪实例接流量”这条底线。

调度器本身并不执行“杀掉进程”的动作,它只是把目标实例数和新状态通过接口下发给实例管理模块。比如要缩容,调度器向某实例发送“drain”指令,实例自行决定如何优雅退出。这种模式在微服务架构里也常用,迁移到昇思大模型推理场景后,唯一的区别是状态管理的粒度更细、动作更重。

5. 线上常见问题和排查技巧实录

5.1 实例一直处于INITIALIZING,请求却开始打进来了

有一次线上扩容,调度器判断QPS上涨,拉起一个新的昇思推理实例。由于模型权重很大,加载过程持续了两分多钟。但接入层没有等我自定义的就绪状态上报,而是根据端口探测成功就把新实例纳入了负载均衡池。结果请求打过去后,实例还在执行图编译,每个请求都在阻塞等待,导致这批请求全部超时。

排查时我先看了注册中心的心跳记录,确认实例确实没有上报Ready状态。接着看接入层的服务发现逻辑,发现它只探测TCP端口,没有读取生命周期状态。修复方案也很明确:接入层的健康检查必须与生命周期状态联动,只有state=READY的实例才能出现在后端节点列表里。另外在调度器里也加了保护,即使接入层漏了,控制面也不会把非READY实例的地址下发给网关。

5.2 缩容把正在推理的实例杀掉了

另一次事故发生在半夜流量低谷。调度器检测到某个实例的空闲率很高,就触发了缩容。旧脚本写得很粗暴,直接向实例进程发送SIGKILL,结果把一批正在处理的长请求全部中断,下游调用方疯狂重试,反而把流量又打起来了。

这个问题本质上是缩容没有走生命周期管理里的排空流程。后来我把下线动作统一改为先摘流量,再发drain指令,实例在RequestTracker里等待活跃请求归零。只有两种情况允许强制结束:一是活跃请求数已经归零但进程没有退出;二是排空等待超过配置的30秒时间窗口。这个兜底逻辑避免了一台实例卡死永远占着显存的问题。所以配置force_kill_after_timeout: true是有必要的,关键在于这个timeout必须给足,尤其大模型长请求可能需要处理几十秒,只等5秒是不够的。

5.3 升级后显存暴涨,回滚也救不回来

有一次发布新版本大模型,旧实例组在线,新实例组开始拉取新权重。由于新版本模型输入的max_batch或context长度配置比旧版本大很多,加上新旧两个实例组同时存在,设备显存被直接占满。我在监控上看到显存使用曲线直接到顶,回滚指令发出去之后,新实例组里的进程卡在OOM边缘动弹不得。

回滚救不回来是因为资源已经被占满了,调度器想缩容新实例,但注册中心里新实例还是INITIALIZING或ERROR状态,容错逻辑没处理好。后来我在版本发布前强制加了一步“资源余量检查”,如果当前设备显存空闲小于新版本所需空余量,则直接拒绝发布任务。同时把新旧实例组的总资源占用纳入调度器视图,避免发布期间超卖。另一个教训是模型版本的max_seq_len、KV Cache策略应该和权重文件一起打包,不能只换权重不换配置,否则升级动作本身就会因为显存配置错误而失控。

5.4 模型加载阶段与请求并发互相干扰

大模型推理实例在加载权重时,会占用设备的大量带宽和内存通道。如果这时候同机的其他实例还在高并发处理推理请求,加载速度会变得极慢,甚至出现加载超时。我在实际运行中见过,两个实例在同昇腾设备上,一个在做模型编译,一个在持续推理,前者的加载时间比常态多了三倍。

处理办法是引入全局的“加载互斥”策略。一个设备上同一时刻只允许一个实例进入权重加载或编译阶段。这需要在调度器层面分配实例时检查设备状态。如果已有实例处于INITIALIZING并占用设备,新的扩容请求会排队等待,而不是一窝蜂全塞上去。另外,初始化阶段可以降低实例对设备带宽的占用优先级,比如限制加载线程数,减少对正常推理请求的影响。

5.5 线上生命周期问题速查表

症状可能原因快速排查方式长期修复方向
请求偶发超时,错误率上升未就绪实例被调度查看注册中心实例状态分布,确认是否有非READY实例在服务列表接入层健康检查与状态机联动
缩容后大量请求失败直接kill实例,未走排空检查实例日志是否有SIGKILL记录统一走DRAINING+超时机制
发布期间显存暴涨新旧实例组共存,无资源余量检查查看设备显存曲线发布节点发布前强制资源余量确认
实例无法成功回滚新进程OOM,调度器无法管理查看新实例状态是否ERROR或STOPPED支持强制回收和资源清理
加载耗时增长同设备并发加载/推理抢占带宽检查设备活跃任务列表全局加载互斥和线程限制
心跳正常但推理卡死进程内死锁或显存碎片抓取线程栈,观察活跃请求计数完善进程级健康探针和自动重启

真正把昇思大模型推理系统的生命周期管理做扎实,绝不是写几个状态枚举和下线脚本就够了。它需要把“实例是什么、何时能服务、何时不能服务、怎么退出”这个闭环,通过架构分层和代码逻辑固定下来。我栽过几次跟头之后,最大的体会是:不要迷信“先启动起来再说”,很多线上故障都源于状态没有理清。你花在设计状态机和排空流程上的时间,远比之后抢救事故的时间少得多。另一个很实用的建议是,一开始就为每个实例的日志打上instance_id和model_version,这样从注册、扩缩容到下线,所有问题都能快速关联到具体实例进程,排查效率会高非常多。希望这套流程和代码能给你在做昇思推理系统时提供一个可以落地的起点。

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

深入解析Telegram for macOS:终极加密通讯客户端的完整指南

深入解析Telegram for macOS&#xff1a;终极加密通讯客户端的完整指南 Telegram for macOS是一款备受欢迎的加密通讯客户端&#xff0c;为Mac用户提供了安全、便捷的即时通讯体验。尽管这是基于Objective-C的macOS客户端版本&#xff0c;目前已不再官方支持&#xff0c;但其核…

作者头像 李华
网站建设 2026/9/8 18:41:31

资深财税顾问详解长沙擅长高企账务机构的筛选标准与考量维度

同样是代账&#xff0c;为什么有的公司做出来的账&#xff0c;审计、科委、税务一看就过&#xff1f;有的却反复退回补材料&#xff1f;不少企业主说“我们也是找的代账公司”&#xff0c;结果申报高新时研发费用归集完全不合规&#xff0c;甚至被税务预警。问题的关键不在“有…

作者头像 李华
网站建设 2026/9/8 18:40:42

初创公司为什么要做测试流程诊断?

很多初创公司“有测试”&#xff0c;可能是一个兼职测试的开发者&#xff0c;或是让产品负责人自己点一点&#xff0c;但这些测试活动缺乏系统性&#xff0c;没有风险分析、没有优先级排序、没有明确的退出标准&#xff0c;结果导致核心功能频繁出Bug&#xff0c;而非核心功能花…

作者头像 李华
网站建设 2026/9/8 18:40:04

Storybook Args 五步上手:从一行参数对象到全框架实时联动

Storybook Args 五步上手&#xff1a;从一行参数对象到全框架实时联动 【免费下载链接】storybook Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation 项目地址: https://gitcode.com/GitHub_Trending/st/sto…

作者头像 李华
网站建设 2026/9/8 18:39:09

基于FPGA的DDS信号发生器:完整工程解析与实战

简介&#xff1a;基于FPGA的DDS信号发生器是一套围绕直接数字频率合成技术的Verilog学习资源&#xff0c;面向FPGA入门及进阶开发者&#xff0c;帮助理解DDS原理、频率控制字配置与波形生成实现。压缩包共2808个文件&#xff0c;约700MB&#xff0c;类型覆盖极广&#xff1a;既…

作者头像 李华
网站建设 2026/9/8 18:39:05

Java 基础语法与常见问题:接口、抽象类与泛型详解

1. Java 基础语法要点 Java 是一门面向对象的编程语言,其基础语法是学习后续所有高级特性的前提。下面从数据类型、运算符、流程控制、方法定义和面向对象基础几个方面梳理核心要点。 1.1 基本数据类型与引用类型 Java 的数据类型分为两大类:基本数据类型和引用类型。基本…

作者头像 李华