news 2026/4/23 13:48:19

LazyLLM黑科技 | LazyLLM Launcher:优雅解决异构算力平台的部署难题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LazyLLM黑科技 | LazyLLM Launcher:优雅解决异构算力平台的部署难题

1. 背景与问题

在真实工程环境里,算力平台几乎从来不是单一、稳定的。

公司内部,可能同时维护着多套集群;不同团队用着不同的调度系统;业务一调整,平台就升级、迁移,甚至整体更换。而一旦对外部署或交付给客户,运行环境的不确定性只会更高。不同平台之间,往往在这些地方差异明显:

  • 作业提交方式不同:有的用srun,有的用kubectl,有的则是云厂商的专有 CLI。
  • 资源申请参数不一致:GPU、CPU、内存的声明方式在不同系统中大相径庭。
  • 调度系统和作业生命周期各有一套规则:状态机、日志获取、任务取消的机制完全不同。

面对这些差异,开发者往往需要在业务代码或脚本中逐一适配,结果就是——部署逻辑侵入业务代码。当平台差异直接反映在代码层时,问题会迅速放大,常见情况是:

  • 为不同平台各写一套启动脚本。
  • 业务代码里混进调度参数和平台判断(大量的if-else)。
  • 一换环境,就得整体重改部署逻辑。

结果是:平台一变,业务跟着改;部署本身比功能还复杂,极大地拖慢了研发和交付效率。

2. 难点

要在框架层面彻底解决这个问题,业界通常面临以下技术难点:

  1. API与交互方式的鸿沟:Slurm 是基于命令行的 HPC(High Performance Computing) 调度系统,Kubernetes 是基于声明式 API 的容器编排系统,而各种云平台(如 Sensecore)又有自己定制的工具链。将它们统一到一个抽象层非常困难。
  2. 网络与通信机制的差异:本地运行只需localhost通信;Slurm 多节点需要处理 MPI 或 PyTorch 分布式环境变量;K8s 则需要动态创建 Service、Gateway 和路由规则来暴露端点。
  3. 状态同步与日志流:如何以非阻塞的方式,统一获取不同平台下作业的实时状态(排队中、运行中、失败等)和标准输出日志。
  4. 生命周期与资源回收:分布式任务极易产生“僵尸进程”。当主控脚本退出或被强制终止时,如何确保远程集群上的任务被干净地清理,避免昂贵的算力资源泄露。

3. LazyLLM的解决方案

为了解决上述痛点,LazyLLM 在lazyllm/launcher中引入了独立的Launcher 体系,将运行平台差异从业务逻辑中彻底剥离。在 LazyLLM 中,职责分工非常清楚:

  • 模型与流程:只描述要执行的计算逻辑(如大模型微调、推理服务启动)。
  • Launcher:负责运行平台、资源调度和作业生命周期。

这种设计带来三个直接效果:

  1. 开箱即用:已支持的平台,只需要通过配置选择对应的 Launcher。
  2. 极易扩展:新平台或小众平台,只需继承 Launcher 基类实现调度逻辑。
  3. 代码解耦:不改框架主体,也不动业务代码。

目前,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。为了更好地理解整个系统的运作流转,我们需要理清TrainableModuleComponentLauncher之间的三层递进关系:

  1. TrainableModule(业务编排层)
    • 定位:面向用户的顶层入口,负责管理模型全生命周期(微调、部署、评测等)。
    • 职责:它不关注具体的命令细节,而是充当一个高级容器(Facade)。它根据用户设定的mode(如finetune,deploy),将数据处理、模型训练、服务部署等环节组装成一个Pipeline
    • 交互:它实例化底层的Component(如Vllm,Llamafactory),并将用户配置的资源参数(如 GPU 数量)转换为具体的Launcher对象注入给这些组件。
  2. Component(逻辑执行层)
    • 定位:具体功能的执行单元,如模型推理服务(vLLM)、微调任务(LlamaFactory)。
    • 职责:它们知道“做什么”。Component 负责将业务请求转化为具体的执行命令(Command),例如拼装python -m vllm.entrypoints.api_server ...命令行字符串。它不关心命令是在本地 shell 跑,还是在 K8s 的 Pod 里跑。
    • 交互:它持有Launcher实例。当 Component 被调用时,它会生成基础命令对象(LazyLLMCMD),然后请求 Launcher 将其包装为作业(Job)并提交执行。
  3. Launcher(资源调度层)
    • 定位:连接框架与异构算力平台的适配器,如ScoLauncherSlurmLauncherK8sLauncher
    • 职责:它们知道“在哪做”以及“怎么做”。它们接管 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=k8s

4.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 命令包装为目标平台可执行的命令(如加上bsubdocker 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.pyJob基类中,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大胆地完全重写了_startstop方法,绕过了基类的子进程模型。
    • 资源编排与网关****映射:它利用 Kubernetes Python Client,将业务组件的需求在内存中动态转化为 K8s 的DeploymentJob规范(Spec)。针对推理部署(inference类型的组件),它不仅挂载 NFS/HostPath 存储卷,还会自动且原子化地创建对应的Service,甚至自动配置 IstioGatewayHTTPRoute。这意味着,当你用 K8sLauncher 启动一个大模型时,它拿到的不仅仅是后台进程,而是一个立即可被外部世界访问的 HTTP URL。

5.3 优雅的全局资源清理机制 (__init__.pylauncher/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

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

Jable视频下载工具:3分钟掌握永久保存高清视频的完整方案

Jable视频下载工具&#xff1a;3分钟掌握永久保存高清视频的完整方案 【免费下载链接】jable-download 方便下载jable的小工具 项目地址: https://gitcode.com/gh_mirrors/ja/jable-download 你是否曾经遇到过这样的情况&#xff1a;精心收藏的Jable.tv视频突然无法访问…

作者头像 李华
网站建设 2026/4/23 13:45:26

你的W25Q128驱动稳定吗?聊聊HAL库SPI读写W25Q128的三大坑与优化技巧

W25Q128驱动稳定性实战&#xff1a;HAL库SPI的三大隐形陷阱与工业级优化方案 当你以为W25Q128驱动已经完美运行时&#xff0c;是否遇到过这些诡异现象&#xff1a;系统运行几天后突然数据错乱&#xff1f;高速连续写入时SPI总线莫名其妙崩溃&#xff1f;或是芯片偶尔进入"…

作者头像 李华
网站建设 2026/4/23 13:36:21

把 SAP HANA 到 SAP HANA 的 JWT 单点登录链路真正搭明白,聊透 Smart Data Access 里的环境配置

前阵子我在梳理一条典型的联邦访问链路,本地 SAP HANA 负责建模和发起查询,远端 SAP HANA 负责提供真实数据。表面上看,大家最关心的是 remote source 该怎么建,virtual table 该怎么用。可一旦要把共享技术用户换成按人传递身份的访问方式,真正难啃的地方就落到了 JWT SS…

作者头像 李华