Slime异步原理(单例设计模式)4

简单理解GenerateState为什么必须是单例

想象一个场景:你是一个项目经理,需要协调100个工人同时完成任务。

如果不是单例(每个任务都有新的"你")

python 复制代码
# 问题场景:每个函数调用都创建新的GenerateState
async def task1():
    me1 = GenerateState(args)  # 第一个你,不知道其他任务
    me1.semaphore.acquire()    # 只控制自己的并发
    
async def task2():
    me2 = GenerateState(args)  # 第二个你,也不知道其他任务
    me2.semaphore.acquire()    # 又是一个独立的并发控制

问题

  • 100个"你"各自为政,无法控制总人数
  • 每个"你"都要重新学习工作方法(加载tokenizer)
  • 无法统一停止所有任务

使用单例(只有一个"你")

python 复制代码
# 单例确保所有任务共享同一个状态
async def task1():
    me = GenerateState(args)   # 唯一的你
    me.semaphore.acquire()     # 控制全局并发
    
async def task2():
    me = GenerateState(args)   # 还是那个你
    me.semaphore.acquire()     # 继续控制全局并发

优势

  • 一个"你"知道所有任务进度
  • 只需要学习一次工作方法
  • 可以一键停止所有任务

具体代码对比

问题代码(非单例)

python 复制代码
# 每次调用都创建新实例
def submit_tasks():
    state1 = GenerateState(args)
    state1.pendings.add(task1)  # 只有state1知道这个任务
    
    state2 = GenerateState(args)  
    state2.pendings.add(task2)  # 只有state2知道这个任务
    
    # 问题:无法等待所有任务完成
    asyncio.wait(state1.pendings)  # 只等待state1的任务

正确代码(单例)

python 复制代码
# 所有调用共享同一个实例
def submit_tasks():
    state = GenerateState(args)    # 第一次调用创建
    state.pendings.add(task1)      # 添加到共享集合
    
    state = GenerateState(args)    # 返回同一个实例
    state.pendings.add(task2)      # 继续添加到共享集合
    
    # 正确:可以等待所有任务
    asyncio.wait(state.pendings)   # 等待所有任务

核心原因总结

  1. 并发控制 :需要一个全局的"计数器"来限制同时运行的任务数 1
  2. 资源节约 :tokenizer加载很耗时,只需要加载一次 2
  3. 状态同步 :所有任务需要知道总共还需要生成多少样本 3
  4. 统一控制 :需要能够一次性停止所有正在运行的任务 4

单例模式通过SingletonMeta元类实现,确保无论调用多少次GenerateState(args),都返回同一个实例 5

Notes

  • 每次rollout完成后会调用reset()清理状态,为下次rollout做准备 6
  • 这种设计是异步编程中管理共享状态的常见模式

Wiki pages you might want to explore:

Citations

File: slime/rollout/sglang_rollout.py (L52-52)

python 复制代码
        self.tokenizer = AutoTokenizer.from_pretrained(args.hf_checkpoint, trust_remote_code=True)

File: slime/rollout/sglang_rollout.py (L53-55)

python 复制代码
        self.semaphore = asyncio.Semaphore(
            args.sglang_server_concurrency * args.rollout_num_gpus // args.rollout_num_gpus_per_engine
        )

File: slime/rollout/sglang_rollout.py (L74-77)

python 复制代码
    def reset(self) -> None:
        self.remaining_batch_size = 0
        self.pendings = set()
        self.aborted = False

File: slime/rollout/sglang_rollout.py (L298-300)

python 复制代码
    state = GenerateState(args)
    assert not state.aborted
    state.aborted = True

File: slime/rollout/sglang_rollout.py (L411-412)

python 复制代码
    # reset the global state to prevent effects on the next rollout or eval.
    state.reset()

File: slime/utils/misc.py (L21-32)

python 复制代码
class SingletonMeta(type):
    """
    A metaclass for creating singleton classes.
    """

    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

简要回答

重复调用 GenerateState(args) 是有意的设计,虽然看起来多次创建,但单例模式确保始终返回同一个实例,让代码更简洁且不需要传递状态参数。 1

详细解释

设计理念

这种设计遵循"随处获取"模式,任何需要访问全局状态的函数都可以直接调用 GenerateState(args),而不需要:

  • 将state作为参数层层传递
  • 维护全局变量
  • 担心实例重复创建(单例保证)

具体调用场景

在代码的多个关键位置都会调用 GenerateState(args)

python 复制代码
# 1. 单个样本生成
async def generate(args, sample, sampling_params):
    state = GenerateState(args)  # 获取tokenizer和采样参数 [1](#17-0) 

# 2. 生成+奖励计算
async def generate_and_rm(args, sample, sampling_params):
    state = GenerateState(args)  # 获取信号量进行并发控制 [2](#17-1) 

# 3. 组生成
async def generate_and_rm_group(args, group, sampling_params):
    state = GenerateState(args)  # 检查中止状态 [3](#17-2) 

# 4. 中止操作
async def abort(args, rollout_id):
    state = GenerateState(args)  # 设置全局中止标志 [4](#17-3) 

单例保证

SingletonMeta 元类确保无论调用多少次,都返回同一个实例: 5

python 复制代码
def __call__(cls, *args, **kwargs):
    if cls not in cls._instances:
        instance = super().__call__(*args, **kwargs)
        cls._instances[cls] = instance
    return cls._instances[cls]  # 始终返回同一个实例

优势对比

传统方式(需要传递参数)

python 复制代码
async def generate(args, sample, sampling_params, state, tokenizer, semaphore):
    # 需要传递很多参数
    pass

async def generate_and_rm(args, sample, sampling_params, state, tokenizer, semaphore):
    # 又要传递一遍
    pass

单例方式(随处获取)

python 复制代码
async def generate(args, sample, sampling_params):
    state = GenerateState(args)  # 直接获取,无需传递
    # state包含所有需要的资源

async def generate_and_rm(args, sample, sampling_params):
    state = GenerateState(args)  # 同一个实例,同样获取

实际执行流程

第一次调用
GenerateState(args) 创建新实例
存储到 _instances 返回实例引用 第二次调用
GenerateState(args) 检查 _instances 返回已存储的实例 第N次调用
GenerateState(args) 检查 _instances 返回同一个实例

Notes

  • 这种模式在异步编程中很常见,避免了复杂的参数传递
  • 每次rollout完成后会调用 state.reset() 清理状态 6
  • 虽然代码中多次调用,但实际只创建一次实例,性能开销极小

Wiki pages you might want to explore:

Citations

File: slime/rollout/sglang_rollout.py (L95-98)

python 复制代码
async def generate(args: Namespace, sample: Sample, sampling_params: dict[str, Any]) -> Sample:
    """Generate using traditional SGLang router with token-based workflow"""
    state = GenerateState(args)
    url = f"http://{args.sglang_router_ip}:{args.sglang_router_port}/generate"

File: slime/rollout/sglang_rollout.py (L228-231)

python 复制代码
    state = GenerateState(args)

    # generate
    async with state.semaphore:

File: slime/rollout/sglang_rollout.py (L269-273)

python 复制代码
    args: Namespace, group: list[Sample], sampling_params: dict[str, Any], evaluation: bool = False
) -> list[Sample]:
    state = GenerateState(args)

    if state.aborted:

File: slime/rollout/sglang_rollout.py (L295-299)

python 复制代码
async def abort(args: Namespace, rollout_id: int) -> list[list[Sample]]:
    aborted_samples = []

    state = GenerateState(args)
    assert not state.aborted

File: slime/rollout/sglang_rollout.py (L411-412)

python 复制代码
    # reset the global state to prevent effects on the next rollout or eval.
    state.reset()

File: slime/utils/misc.py (L28-32)

python 复制代码
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]

你观察得非常仔细!这是一个非常好的问题,它揭示了单例模式(Singleton)在实际代码中的工作方式和它的一个有趣特性。

你说得对,从表面上看,state1 = GenerateState(args) 这行代码在 generategenerate_and_rm 等多个函数中被反复调用。这似乎与我们之前讨论的"只初始化一次"相矛盾。

但这里的关键在于 GenerateState 类是如何定义的:

python 复制代码
class GenerateState(metaclass=SingletonMeta):
    """
    The global state for the generation process.
    """
    def __init__(self, args: Namespace) -> None:
        # ...

它使用了 metaclass=SingletonMeta。这正是魔法发生的地方。

metaclass=SingletonMeta 的作用

当一个类使用元类(metaclass)时,元类会控制这个类的创建过程。SingletonMeta 的实现(虽然代码中没有展示,但我们可以推断其行为)通常是这样的:

  1. 它内部维护一个字典或类似的结构,用来存储已经创建的类的实例。例如 _instances = {}

  2. 当你第一次调用 GenerateState(args) 时:

    • 元类检查 _instances 字典,发现里面没有 GenerateState 的实例。
    • 它会正常执行 GenerateState__init__ 方法,创建一个新的实例。
    • 然后,它将这个新创建的实例存入 _instances 字典中 ,通常以类本身作为键(_instances[GenerateState] = new_instance)。
    • 最后,返回这个新实例。
  3. 当你第二次(以及之后每一次) 调用 GenerateState(args) 时:

    • 元类再次检查 _instances 字典。
    • 这一次,它发现字典里已经有 一个 GenerateState 的实例了。
    • 它会直接返回这个已经存在的实例,而根本不会再次执行 __init__ 方法!

所以,结论是:

尽管代码写的是 state = GenerateState(args),看起来像是在创建一个新对象,但由于单例模式的存在,从第二次调用开始,__init__ 方法根本不会被执行 。你得到的 state 变量,永远是第一次调用时创建的那个独一无二的对象。

为什么代码要这么写?

你可能会问,既然总是返回同一个对象,为什么不把它定义成一个全局变量,只在程序启动时初始化一次呢?比如:

python 复制代码
# 在模块顶层
GLOBAL_STATE = None

def initialize_global_state(args):
    global GLOBAL_STATE
    if GLOBAL_STATE is None:
        GLOBAL_STATE = GenerateState(args)

# 在函数内部
def generate(...):
    state = GLOBAL_STATE 
    # ...

这种方法在功能上是等价的,但使用单例模式有几个软件工程上的好处:

  1. 延迟初始化(Lazy Initialization) :单例模式只有在第一次被需要时(即第一次调用 GenerateState(args) 时)才会进行初始化。如果程序在某些分支下根本不需要这个状态对象,那么昂贵的初始化就不会发生。而全局变量通常在模块加载时就初始化了。

  2. 封装和接口清晰GenerateState(args) 这个调用方式看起来就像一个普通的类实例化。它向函数的使用者传递了一个清晰的信号:"这个函数依赖于 GenerateState"。使用者不需要知道全局变量的存在和命名。这降低了模块间的耦合度。如果未来决定不再使用单例,只需要修改 SingletonMeta 的实现,而不需要修改所有调用它的函数。

  3. 避免全局命名空间污染 :过度使用全局变量会让代码变得难以理解和维护,因为任何地方都可能修改它。单例将状态封装在一个类中,通过一个统一的入口 (GenerateState(args)) 来访问,更符合面向对象的思想。

总结

  • 表面现象 :代码中确实在多个地方调用了 GenerateState(args)
  • 底层机制 :由于 metaclass=SingletonMeta 的作用,__init__ 方法只在第一次调用时执行一次 。后续所有调用都会跳过初始化,并立即返回第一次创建的那个唯一的实例
  • 效果 :所有调用 GenerateState(args) 的地方,最终都指向了同一个内存中的对象,共享同一个 tokenizer、同一个 semaphore、同一个 pendings 集合等。
  • 好处:这种写法既享受了单例带来的性能和状态一致性优势,又保持了代码接口的清晰和封装性,是一种非常优雅和常见的软件设计模式。
相关推荐
e***74953 小时前
Modbus报文详解
服务器·开发语言·php
lly2024063 小时前
ASP 发送电子邮件详解
开发语言
小徐敲java3 小时前
python使用s7协议与plc进行数据通讯(HslCommunication模拟)
开发语言·python
likuolei3 小时前
XSL-FO 软件
java·开发语言·前端·数据库
6***37943 小时前
PHP在电商中的BigCommerce
开发语言·php
猫头虎3 小时前
如何解决 pip install 编译报错 fatal error: hdf5.h: No such file or directory(h5py)问题
人工智能·python·pycharm·开源·beautifulsoup·ai编程·pip
Dev7z3 小时前
基于Matlab的多制式条形码识别与图形界面(GUI)系统设计与实现
开发语言·matlab
合作小小程序员小小店3 小时前
桌面开发,在线%信息管理%系统,基于vs2022,c#,winform,sql server数据。
开发语言·数据库·sql·microsoft·c#
FL16238631293 小时前
ONNX RuntimeC++ 静态库下载安装和使用教程
开发语言·c++