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 来指定全局默认的运行平台。设置后,框架内所有的任务都会默认提交到该平台,无需修改任何代码:

# 使用本地环境运行(默认)
export LAZYLLM_DEFAULT_LAUNCHER=empty

# 提交到 Slurm 集群运行
export LAZYLLM_DEFAULT_LAUNCHER=slurm

# 提交到 Sensecore 云平台运行
export LAZYLLM_DEFAULT_LAUNCHER=sco

# 提交到 Kubernetes 集群运行
export LAZYLLM_DEFAULT_LAUNCHER=k8s






4.2 基础用法:为内置模块手动指定 Launcher

如果需要更精细的控制(例如不同任务跑在不同平台,或申请不同数量的 GPU),可以在代码中通过传入 lazyllm.launchers.xxxx 来手动覆盖默认设置。

以下是基于 TrainableModule 构造者模式的配置示例:

import lazyllm
from lazyllm import deploy, 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 体系。

代码示例:

import lazyllm

lazyllm.component_register.new_group('demo')

@lazyllm.component_register('demo')
def test(input):
    return f'input is {input}'

@lazyllm.component_register.cmd('demo')
def test_cmd(input):
    return f'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 示例:

import time
import subprocess
from lazyllm.launcher.base import LazyLLMLaunchersBase, Job, Status

class MyCloudLauncher(LazyLLMLaunchersBase):
    
    class Job(Job):
        def __init__(self, cmd, launcher, *, sync=True):
            super().__init__(cmd, launcher, sync=sync)
            self.queue_name = launcher.queue_name
            
        def _wrap_cmd(self, cmd):
            # 将普通命令包装为 MyCloud 的提交命令
            return f"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" 
            
        @property
        def status(self):
            # 调用云平台命令查询状态并映射到 Status 枚举
            if not self.jobid: return Status.Failed
            out = subprocess.check_output(["mycloud", "status", self.jobid]).decode()
            if "RUNNING" in out: return Status.Running
            if "COMPLETED" in out: return Status.Done
            if "FAILED" in out: return Status.Failed
            return Status.Pending
            
        def stop(self):
            # 清理任务
            if self.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 = sync

    def makejob(self, cmd):
        return MyCloudLauncher.Job(cmd, launcher=self, sync=self.sync)

    def launch(self, job):
        job.start()
        if self.sync:
            # 同步模式下,阻塞等待任务运行完成
            while job.status in (Status.Pending, Status.Running, Status.InQueue):
                time.sleep(5)
            job.stop() # 运行结束后确保清理
        return job.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,甚至自动配置 Istio GatewayHTTPRoute。这意味着,当你用 K8sLauncher 启动一个大模型时,它拿到的不仅仅是后台进程,而是一个立即可被外部世界访问的 HTTP URL。

5.3 优雅的全局资源清理机制 (__init__.pylauncher/base.py)

分布式任务最怕的就是主进程崩溃导致远程节点上的任务变成“孤儿”,白白消耗昂贵的 GPU 资源。在 LazyLLM 中,每一项被提交的 Job 都会被注册入各自 Launcher 的 all_processes 全局字典中。

LazyLLM 利用 Python 的 atexit 模块注册了全局清理函数:

import atexit

def cleanup():
    for m in (EmptyLauncher, SlurmLauncher, ScoLauncher, K8sLauncher): # 伪代码示例
        for launcher_id in list(m.all_processes.keys()):
            for k, v in m.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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐