1. 背景与问题
在真实工程环境里,算力平台几乎从来不是单一、稳定的。
公司内部,可能同时维护着多套集群;不同团队用着不同的调度系统;业务一调整,平台就升级、迁移,甚至整体更换。而一旦对外部署或交付给客户,运行环境的不确定性只会更高。不同平台之间,往往在这些地方差异明显:
- 作业提交方式不同:有的用
srun,有的用kubectl,有的则是云厂商的专有 CLI。 - 资源申请参数不一致:GPU、CPU、内存的声明方式在不同系统中大相径庭。
- 调度系统和作业生命周期各有一套规则:状态机、日志获取、任务取消的机制完全不同。
面对这些差异,开发者往往需要在业务代码或脚本中逐一适配,结果就是——部署逻辑侵入业务代码。当平台差异直接反映在代码层时,问题会迅速放大,常见情况是:
- 为不同平台各写一套启动脚本。
- 业务代码里混进调度参数和平台判断(大量的
if-else)。 - 一换环境,就得整体重改部署逻辑。
结果是:平台一变,业务跟着改;部署本身比功能还复杂,极大地拖慢了研发和交付效率。
2. 难点
要在框架层面彻底解决这个问题,业界通常面临以下技术难点:
- API与交互方式的鸿沟:Slurm 是基于命令行的 HPC(High Performance Computing) 调度系统,Kubernetes 是基于声明式 API 的容器编排系统,而各种云平台(如 Sensecore)又有自己定制的工具链。将它们统一到一个抽象层非常困难。
- 网络与通信机制的差异:本地运行只需
localhost通信;Slurm 多节点需要处理 MPI 或 PyTorch 分布式环境变量;K8s 则需要动态创建 Service、Gateway 和路由规则来暴露端点。 - 状态同步与日志流:如何以非阻塞的方式,统一获取不同平台下作业的实时状态(排队中、运行中、失败等)和标准输出日志。
- 生命周期与资源回收:分布式任务极易产生“僵尸进程”。当主控脚本退出或被强制终止时,如何确保远程集群上的任务被干净地清理,避免昂贵的算力资源泄露。
3. LazyLLM的解决方案
为了解决上述痛点,LazyLLM 在lazyllm/launcher中引入了独立的Launcher 体系,将运行平台差异从业务逻辑中彻底剥离。在 LazyLLM 中,职责分工非常清楚:
- 模型与流程:只描述要执行的计算逻辑(如大模型微调、推理服务启动)。
- Launcher:负责运行平台、资源调度和作业生命周期。
这种设计带来三个直接效果:
- 开箱即用:已支持的平台,只需要通过配置选择对应的 Launcher。
- 极易扩展:新平台或小众平台,只需继承 Launcher 基类实现调度逻辑。
- 代码解耦:不改框架主体,也不动业务代码。
目前,LazyLLM 已内置多种 Launcher,用于覆盖常见运行环境:本地执行(EmptyLauncher)、Kubernetes 集群(K8sLauncher)、Slurm 调度集群(SlurmLauncher)以及云平台部署(ScoLauncher)。这些 Launcher 共享统一的作业生命周期抽象,上层模块始终用同一种方式被管理和调度。
同一个 Component,既可以在本地直接运行,也可以通过指定 Launcher 提交到云平台执行。业务代码不变,运行位置由 Launcher 动态决定。
3.1 宏观架构:ComponentBase 与 Launcher 的协同交互
为了实现上述的解耦,LazyLLM 在架构设计上明确了ComponentBase(组件基类)和Launcher(启动器基类)的分工与协同关系:
- 职责划分:
ComponentBase(如Vllm,LlamafactoryFinetune)负责定义“做什么”,它们关注业务逻辑,生成与平台无关的基础执行命令;而 Launcher负责定义“怎么做”和“在哪做”,将基础命令包装成特定平台可执行的格式。 - 交互流程:当用户调用组件时,组件先调用自身的
cmd方法生成基础命令(LazyLLMCMD),然后调用launcher.makejob(cmd)创建特定平台的任务对象,最后通过launcher.launch(job)提交执行。后台的Job对象会异步处理状态同步与日志回传。
整个调用链路的架构如下所示:
3.2 顶层封装:TrainableModule、Component 与 Launcher 的三层架构
在实际开发中,用户最常接触的 API 是TrainableModule。为了更好地理解整个系统的运作流转,我们需要理清TrainableModule、Component和Launcher之间的三层递进关系:
- TrainableModule(业务编排层):
- 定位:面向用户的顶层入口,负责管理模型全生命周期(微调、部署、评测等)。
- 职责:它不关注具体的命令细节,而是充当一个高级容器(Facade)。它根据用户设定的
mode(如finetune,deploy),将数据处理、模型训练、服务部署等环节组装成一个Pipeline。 - 交互:它实例化底层的
Component(如Vllm,Llamafactory),并将用户配置的资源参数(如 GPU 数量)转换为具体的Launcher对象注入给这些组件。
- Component(逻辑执行层):
- 定位:具体功能的执行单元,如模型推理服务(
vLLM)、微调任务(LlamaFactory)。 - 职责:它们知道“做什么”。Component 负责将业务请求转化为具体的执行命令(Command),例如拼装
python -m vllm.entrypoints.api_server ...命令行字符串。它不关心命令是在本地 shell 跑,还是在 K8s 的 Pod 里跑。 - 交互:它持有
Launcher实例。当 Component 被调用时,它会生成基础命令对象(LazyLLMCMD),然后请求 Launcher 将其包装为作业(Job)并提交执行。
- 定位:具体功能的执行单元,如模型推理服务(
- Launcher(资源调度层):
- 定位:连接框架与异构算力平台的适配器,如
ScoLauncher、SlurmLauncher、K8sLauncher。 - 职责:它们知道“在哪做”以及“怎么做”。它们接管 Component 生成的基础命令,根据目标平台的协议进行二次包装(如加上
srun前缀或生成 K8s YAML),处理资源申请、作业提交、状态监控和日志回传。 - 交互:它生成特定平台的
Job对象,并管理该对象的生命周期。 这种三层架构使得业务流转、算法逻辑与算力资源实现了正交解耦。下图展示了这三者在类结构上的关系及运行时的调用流向:
- 定位:连接框架与异构算力平台的适配器,如
用户可以在TrainableModule中为微调和部署阶段分别指定不同的Launcher(例如微调在 Slurm 集群,部署在 K8s 集群),框架会自动完成跨平台的无缝衔接。
4. 该解决方案下的代码示例及预期产出
在 LazyLLM 中,切换运行平台既可以通过全局环境变量一键切换,也可以在代码中精细化指定。
4.1 全局配置:通过环境变量设置默认 Launcher
最简单的方式是在运行前通过环境变量LAZYLLM_DEFAULT_LAUNCHER来指定全局默认的运行平台。设置后,框架内所有的任务都会默认提交到该平台,无需修改任何代码:
# 使用本地环境运行(默认)exportLAZYLLM_DEFAULT_LAUNCHER=empty# 提交到 Slurm 集群运行exportLAZYLLM_DEFAULT_LAUNCHER=slurm# 提交到 Sensecore 云平台运行exportLAZYLLM_DEFAULT_LAUNCHER=sco# 提交到 Kubernetes 集群运行exportLAZYLLM_DEFAULT_LAUNCHER=k8s4.2 基础用法:为内置模块手动指定 Launcher
如果需要更精细的控制(例如不同任务跑在不同平台,或申请不同数量的 GPU),可以在代码中通过传入lazyllm.launchers.xxxx来手动覆盖默认设置。
以下是基于TrainableModule构造者模式的配置示例:
importlazyllmfromlazyllmimportdeploy,finetune,launchers# 1. 本地部署一个大模型 (显式指定 empty launcher)m1=lazyllm.TrainableModule('qwen2.5-7b-instruct')\.mode('deploy')\.deploy_method((deploy.vllm,{'launcher':launchers.empty()}))# 2. Slurm 部署一个 2 卡大模型m2=lazyllm.TrainableModule('qwen2.5-7b-instruct')\.mode('deploy')\.deploy_method((deploy.vllm,{'tensor_parallel_size':2,'launcher':launchers.slurm(ngpus=2)}))# 3. SCO 上微调并部署一个大模型m3=lazyllm.TrainableModule('qwen2.5-7b-instruct','./save_path')\.mode('finetune')\.trainset('dataset.json')\.finetune_method((finetune.llamafactory,{'launcher':launchers.sco(ngpus=4,sync=True)}))\.deploy_method((deploy.vllm,{'launcher':launchers.sco(ngpus=1)}))4.3 进阶用法:与自定义 Component 配合
对于用户自己注册的组件,同样可以无缝接入 Launcher 体系。
代码示例:
importlazyllm lazyllm.component_register.new_group('demo')@lazyllm.component_register('demo')deftest(input):returnf'input is{input}'@lazyllm.component_register.cmd('demo')deftest_cmd(input):returnf'echo input is{input}'# 1. 本地直接运行print(lazyllm.demo.test()(1))# 2. 指定使用 SCO (Sensecore) 云平台 Launcher 运行print(lazyllm.demo.test_cmd(launcher=lazyllm.launchers.sco)(2))预期产出:
input is 1 2026-02-27 10:50:15 lazyllm INFO (lazyllm.launcher.base:122, 2555313): Command: srun -p a800 --workspace-id expert-services --job-name=s_flag_7a51e703 -f pt -r N3lS.Ii.I60.1 -N 1 --priority normal 'source activate lazyllm && export PYTHONPATH=... && echo input is 2' 2026-02-27 10:50:17 launcher INFO (lazyllm.launcher.base:179, 2555313, jobid=pt-0424cyle): job pt-0424cyle submitted successfully, please wait for scheduling! 2026-02-27 10:50:26 launcher INFO (lazyllm.launcher.base:179, 2555313, jobid=pt-0424cyle): job pt-0424cyle scheduled successfully 2026-02-27 10:50:26 launcher INFO (lazyllm.launcher.base:179, 2555313, jobid=pt-0424cyle): pt-d9c24dc59bd042999f548bb07ed3c11a-worker-0 logs: LAZYLLMIP 10.119.29.56 2026-02-27 10:50:26 launcher INFO (lazyllm.launcher.base:179, 2555313, jobid=pt-0424cyle): pt-d9c24dc59bd042999f548bb07ed3c11a-worker-0 logs: input is 2 2026-02-27 10:50:28 launcher INFO (lazyllm.launcher.base:179, 2555313, jobid=pt-0424cyle): id : pt-0424cyle <lazyllm.launcher.sco.ScoLauncher.Job object at 0x7f2475502f50>开发者侧使用说明:开发者在编写业务逻辑时,完全不需要关心代码最终会在哪里运行。当需要将某个组件(如模型推理服务)部署到集群时,只需在实例化或调用该组件时,通过launcher=lazyllm.launchers.slurm()或launcher=lazyllm.launchers.k8s()注入对应的启动器。LazyLLM 会自动接管后续的命令组装、环境配置、任务提交和日志回传。
4.4 高级扩展:如何自定义一个新的 Launcher
得益于 LazyLLM 优秀的抽象设计,接入一个全新的算力平台(例如 LSF 集群、特定的云厂商容器服务等)非常简单。开发者只需要继承LazyLLMLaunchersBase并实现特定的接口即可。
自定义 Launcher 主要分为两步:
第一步:实现自定义的Job类Job类负责具体的命令包装、状态查询和任务终止。你需要继承lazyllm.launcher.base.Job并重写以下核心方法:
_wrap_cmd(self, cmd):将原始 Python 命令包装为目标平台可执行的命令(如加上bsub或docker run)。status(Property):调用目标平台的 API 或命令行,获取任务实时状态,并将其映射为lazyllm.launcher.base.Status枚举(如Running,Done,Failed等)。stop(self):定义如何清理和终止该任务(如执行bkill或调用 API 删除容器),防止资源泄露。_get_jobid(self)(可选):从提交任务的输出中解析并保存任务 ID,供后续状态查询和清理使用。
第二步:实现 Launcher类继承LazyLLMLaunchersBase,主要实现任务的创建与分发逻辑:
__init__:接收平台特有的参数(如队列名、节点数、资源规格等)。makejob(self, cmd):实例化并返回上面定义的自定义Job对象。launch(self, job):调用job.start()提交任务,并根据self.sync决定是否阻塞等待任务完成。
代码示例:
以下是一个接入某假想云平台(MyCloud)的极简 Launcher 示例:
importtimeimportsubprocessfromlazyllm.launcher.baseimportLazyLLMLaunchersBase,Job,StatusclassMyCloudLauncher(LazyLLMLaunchersBase):classJob(Job):def__init__(self,cmd,launcher,*,sync=True):super().__init__(cmd,launcher,sync=sync)self.queue_name=launcher.queue_namedef_wrap_cmd(self,cmd):# 将普通命令包装为 MyCloud 的提交命令returnf"mycloud submit --queue{self.queue_name}'{cmd}'"def_get_jobid(self):# 假设 mycloud submit 会在标准输出打印 "Job submitted: <job_id>"# 实际开发中可通过正则解析 self.ps.stdout 或命令行返回结果self.jobid="mock_job_id_123"@propertydefstatus(self):# 调用云平台命令查询状态并映射到 Status 枚举ifnotself.jobid:returnStatus.Failed out=subprocess.check_output(["mycloud","status",self.jobid]).decode()if"RUNNING"inout:returnStatus.Runningif"COMPLETED"inout:returnStatus.Doneif"FAILED"inout:returnStatus.FailedreturnStatus.Pendingdefstop(self):# 清理任务ifself.jobid:subprocess.run(["mycloud","cancel",self.jobid])def__init__(self,queue_name="default",sync=True,**kwargs):super().__init__()self.queue_name=queue_name self.sync=syncdefmakejob(self,cmd):returnMyCloudLauncher.Job(cmd,launcher=self,sync=self.sync)deflaunch(self,job):job.start()ifself.sync:# 同步模式下,阻塞等待任务运行完成whilejob.statusin(Status.Pending,Status.Running,Status.InQueue):time.sleep(5)job.stop()# 运行结束后确保清理returnjob.return_value编写完成后,由于LazyLLMLaunchersBase使用了元类(LazyLLMRegisterMetaClass)注册机制,你只需在代码中导入该类,即可像内置 Launcher 一样通过launcher=MyCloudLauncher()将组件调度到新平台上运行。
5. 我们是如何做到的(技术剖析)
LazyLLM 的 Launcher 体系在底层做了大量精巧的设计,核心在于统一抽象与平台特化的结合。整个体系重度运用了模板方法(Template Method) 和策略(Strategy)等经典设计模式。
5.1 统一的作业抽象与状态机 (base.py)
所有的 Launcher 都继承自LazyLLMLaunchersBase,并内部实现一个继承自Job的类。Job类是整个体系的核心,它定义了统一的状态机枚举Status:
TBSubmitted(待提交)InQueue(排队中)Running(运行中)Pending(挂起)Done(完成)Cancelled(已取消)Failed(失败)
无论底层是 K8s 的 Pod 状态,还是 Slurm 的squeue状态,最终都会被映射到这个统一的Status枚举中。上层业务代码只需要轮询job.status,就能无差别地监控任务进度。
模板方法模式的运用:在base.py的Job基类中,start()方法被定义为一个模板方法。它固化了任务启动的宏观流程(包括:调用核心启动逻辑、失败重试等待、日志流捕获、处理返回值),并将底层差异抽象为_start()和_wrap_cmd()等内部方法交由子类去实现。
同时,Job基类中通过_enqueue_subprocess_output实现了异步的日志捕获机制。它通过启动后台守护线程 (threading.Thread) 实时读取子进程的stdout,并存入线程安全队列 (Queue)。这使得远程集群的日志能够像本地日志一样实时打印在终端上,极大地提升了调试体验。
5.2 平台特化的命令包装与调度
为了适配不同平台的脾气,LazyLLM 展现了极高的灵活性。各个 Launcher 针对自身平台的特性(Imperative 指令式 vs Declarative 声明式)采取了不同的实现策略:
- SlurmLauncher (
slurm.py)与ScoLauncher (sco.py):- 指令包装机制:这两者属于传统的“命令行调度系统”。它们复用了基类基于
subprocess.Popen的_start逻辑,仅重写_wrap_cmd方法。原始的 Python 命令被精准“穿衣”,包裹上srun -p <partition> ...或云平台特有的环境变量(如 SCO 的torchrun分布式参数渲染)。 - 网络发现:针对集群内难以获取容器 IP 的问题,巧妙地在启动脚本中注入
bash -c "ifconfig | grep inet | awk...",将远程节点分配到的 IP 通过标准输出stdout截获并回传给主控端(利用output_hooks回调),从而实现分布式节点间的互联。
- 指令包装机制:这两者属于传统的“命令行调度系统”。它们复用了基类基于
- K8sLauncher(k8s.py) ——API驱动的极致展现:
- K8s 是基于声明式 API 的系统,无法用简单的
subprocess跑命令行来解决。因此,K8sLauncher.Job大胆地完全重写了_start和stop方法,绕过了基类的子进程模型。 - 资源编排与网关****映射:它利用 Kubernetes Python Client,将业务组件的需求在内存中动态转化为 K8s 的
Deployment或Job规范(Spec)。针对推理部署(inference类型的组件),它不仅挂载 NFS/HostPath 存储卷,还会自动且原子化地创建对应的Service,甚至自动配置 IstioGateway和HTTPRoute。这意味着,当你用 K8sLauncher 启动一个大模型时,它拿到的不仅仅是后台进程,而是一个立即可被外部世界访问的 HTTP URL。
- K8s 是基于声明式 API 的系统,无法用简单的
5.3 优雅的全局资源清理机制 (__init__.py与launcher/base.py)
分布式任务最怕的就是主进程崩溃导致远程节点上的任务变成“孤儿”,白白消耗昂贵的 GPU 资源。在 LazyLLM 中,每一项被提交的Job都会被注册入各自 Launcher 的all_processes全局字典中。
LazyLLM 利用 Python 的atexit模块注册了全局清理函数:
importatexitdefcleanup():formin(EmptyLauncher,SlurmLauncher,ScoLauncher,K8sLauncher):# 伪代码示例forlauncher_idinlist(m.all_processes.keys()):fork,vinm.all_processes[launcher_id]:v.stop()LOG.info(f'killed job:{k}')m.all_processes.pop(launcher_id)atexit.register(cleanup)当 Python 解释器正常退出、抛出未捕获异常或收到终止信号时,cleanup半自动触发。它会遍历所有已实例化的 Launcher 字典,精准调用每个Job子类特化的stop()方法(如触发 Slurm 的scancel、调用 Kubernetes API 发出Deployment的 Delete 请求,或杀死本地的进程树)。这确保了无论框架运行在哪种异构平台上,LazyLLM 都能做到“片叶不沾身”,干净利落地回收所有计算资源。
6. 总结
总而言之,LazyLLM 的 Launcher 体系通过精巧的抽象设计,成功地在复杂的异构算力平台与纯粹的 AI 业务逻辑之间建立了一道优雅的隔离层。
它不仅解决了多平台部署的代码侵入问题,还提供了统一的状态监控、日志回传和资源回收机制。开发者只需聚焦于“做什么”(模型训练、推理等核心逻辑),而将“在哪做、怎么做”的繁琐细节放心地交给 Launcher。这种“一次编写,随处运行”的体验,极大地提升了 AI 应用的研发效率与交付稳定性。
欢迎升级体验 LazyLLM 最新版本,请大家去 github 上点一个免费的 star,支持一下~技术讨论欢迎关注 “LazyLLM” !
LazyLLM 项目仓库链接🔗:
https://github.com/LazyAGI/LazyLLM
https://github.com/LazyAGI/LazyLLM/releases/tag/v0.7.1