vLLM:高性能大语言模型推理框架源码解析与最佳实践

该文章已生成可运行项目,

vLLM:高性能大语言模型推理框架源码解析与最佳实践

目录

  1. 引言
  2. 快速上手
    2.1. 安装配置
    2.2. 基本用法
  3. 核心调用流程分析
    3.1. 总体调用链路概述
    3.2. 核心组件与类层次结构
    3.3. 初始化阶段详细流程
    3.4. 推理阶段详细流程
    3.5. 完整调用链路示例
    3.6. 关键调用路径总结
  4. vLLM 关键工作机制
    4.1. PagedAttention 机制
    4.2. 连续批处理技术
    4.3. CUDA 图捕获与加速
    4.4. KV 缓存管理
    4.5. 内存优化策略
    4.6. 并行计算优化
  5. 架构与类关系图
    5.1. 整体架构概览
    5.2. 核心类结构图
    5.3. 关键组件交互流程
    5.4. 并行与分布式架构
    5.5. 内存管理架构
  6. 高级 Python 语法应用
    6.1. 类型注解与泛型编程
    6.2. 动态类解析与反射机制
    6.3. 方法委托与魔术方法
    6.4. 弱引用与特殊调用模式
    6.5. 类方法与工厂模式
    6.6. 装饰器高级应用
    6.7. 上下文管理与多重上下文
  7. 性能优化与最佳实践
    7.1. 示例应用:构建高性能API服务
    7.2. 性能优化策略
    7.3. 部署最佳实践
    7.4. 常见问题与解决方案
  8. 总结与展望
    8.1. 技术创新点总结
    8.2. 与其他框架对比
    8.3. 未来发展趋势
    8.4. 实践建议

附录

A. 关键源码目录结构
B. 参考资源

1. 引言

vLLM 是一个高性能的大语言模型推理框架,通过创新的 PagedAttention 机制和优化的内存管理,实现了高吞吐量和低延迟的文本生成服务。本文将深入分析 vLLM 的源码,重点关注其调用流程、高级 Python 语法应用以及核心工作机制,帮助读者更好地理解和使用这一强大工具。

2. 快速上手

2.1 安装配置

vLLM 支持多种硬件平台,安装方式也略有不同:

# 基本安装(CUDA 支持)
pip install vllm

# 从源码安装
git clone https://github.com/vllm-project/vllm.git
cd vllm
pip install -e .

2.2 基本用法

下面是一个简单的 vLLM 使用示例,展示了如何加载模型并生成文本:

# 导入必要的库
from vllm import LLM, SamplingParams

# 初始化模型
llm = LLM(model="/opt/data/models/Llama-3.2-1B-Instruct")

# 设置生成参数
sampling_params = SamplingParams(
    max_tokens=100,         # 最大生成长度
    temperature=0.7,        # 控制随机性
    top_p=0.9,              # 控制取样范围
    stop=["</s>", "\n\n"]   # 停止标记
)

# 准备输入提示
prompts = [
    "介绍一下北京的旅游景点",
    "写一首关于春天的诗",
]

# 生成文本
outputs = llm.generate(prompts, sampling_params)

# 打印结果
for output in outputs:
    print(f"Prompt: {
     
     output.prompt}")
    print(f"Generated: {
     
     output.outputs[0].text}")
    print("-" * 50)

3. 核心调用流程分析

3.1 总体调用链路概述

vLLM 框架的调用流程可以分为初始化阶段和推理阶段两个主要部分。整个流程涉及多个核心组件的协同工作,包括 LLM Engine、Worker、Scheduler、ModelRunner 等类。以下是端到端完整调用链路的详细分析:

用户 API 调用
  ↓
LLMEngine 初始化
  ↓ 
模型加载与配置初始化
  ↓
Worker 池初始化
  ↓
请求处理与调度
  ↓
Tokenizer 处理输入
  ↓
ModelRunner 执行推理
  ↓
KV 缓存管理
  ↓
结果获取与后处理
  ↓
返回生成结果

3.2 核心组件与类层次结构

vLLM 的架构由以下核心组件组成:

  1. LLMEngine:整个框架的中枢,负责协调各组件工作
  2. Worker:实际执行模型计算的组件
  3. ModelRunner:负责模型的前向传播计算
  4. Scheduler:请求调度器,决定何时处理哪些请求
  5. SamplingParams:控制文本生成的参数
  6. Request/Response:请求和响应的数据结构
  7. SequenceGroup/Sequence:表示生成序列的数据结构
  8. BlockManager:管理 KV 缓存的物理块
  9. Tokenizer:负责文本标记化
类层次结构图:
LLMEngine
  ├── AsyncEngineDeadlockMonitor
  ├── ModelConfig
  ├── DeviceConfig
  ├── ParallelConfig
  ├── Scheduler
  │    ├── SchedulerConfig
  │    └── SchedulingStrategy
  ├── RequestTracker
  └── WorkerPool
       ├── Worker
       │    ├── ModelRunner
       │    │    ├── CUDAGraph
       │    │    └── AttentionState
       │    └── BlockSpaceManager
       │         └── BlockAllocator
       └── ModelReplica

3.3 初始化阶段详细流程

3.3.1 LLMEngine 初始化

LLMEngine 是整个 vLLM 的核心,负责协调各组件工作。它的初始化流程如下:

def __init__(
    self,
    model_config: ModelConfig,
    scheduler_config: SchedulerConfig,
    device_config: Optional[DeviceConfig] = None,
    parallel_config: Optional[ParallelConfig] = None,
    **kwargs,
) -> None:
    # 1. 设置基本配置
    self.model_config = model_config
    self.scheduler_config = scheduler_config
    if device_config is None:
        device_config = DeviceConfig()
    self.device_config = device_config
    if parallel_config is None:
        parallel_config = ParallelConfig()
    self.parallel_config = parallel_config
    
    # 2. 初始化策略配置
    self.num_nodes = parallel_config.world_size // parallel_config.tensor_parallel_size
    self.distributed_init_method = kwargs.get("distributed_init_method", None)
    self.max_logprobs = kwargs.get("max_logprobs", None)
    
    # 3. 初始化组件
    self.tokenizer = self._init_tokenizer()
    self.schedulers = self._init_schedulers()
    self.request_tracker = RequestTracker()
    
    # 4. 初始化 Worker 池
    self.worker_pool = WorkerPool(
        model_config=model_config,
        parallel_config=parallel_config,
        scheduler_config=scheduler_config,
        device_config=device_config,
        local_rank=parallel_config.local_rank,
    )
    
    # 5. 启动引擎线程
    self._start_background_thread()

初始化过程中的配置详解:

  1. ModelConfig:包含模型相关的配置,如模型路径、类型、量化设置等

    class ModelConfig:
        def __init__(
            self,
            model: str,
            tokenizer: Optional[str] = None,
            tokenizer_mode: str = "auto",
            trust_remote_code: bool = False,
            dtype: str = "auto",
            quantization: Optional[str] = None,
            revision: Optional[str] = None,
            code_revision: Optional[str] = None,
            tokenizer_revision: Optional[str] = None,
            # ... 更多参数
        ) -> None:
            # 初始化模型配置
    
  2. DeviceConfig:包含设备相关配置,如GPU数量、每个GPU的最大内存等

    class DeviceConfig:
        def __init__(
            self,
            device_type: Optional[str] = None,
            max_memory_per_gpu: Optional[Dict[int, str]] = None,
            # ... 更多参数
        ) -> None:
            # 初始化设备配置
    
  3. ParallelConfig:包含并行计算相关配置,如张量并行度、流水线并行度等

    class ParallelConfig:
        def __init__(
            self,
            tensor_parallel_size: int = 1,
            pipeline_parallel_size: int = 1,
            # ... 更多参数
        ) -> None:
            # 初始化并行配置
    
3.3.2 Tokenizer 初始化

Tokenizer 负责将输入文本转换为模型可以处理的 token ID 序列:

def _init_tokenizer(self) -> PreTrainedTokenizer:
    """初始化 tokenizer"""
    # 加载 tokenizer
    tokenizer = get_tokenizer(
        model_name=self.model_config.model,
        tokenizer_name=self.model_config.tokenizer,
        tokenizer_mode=self.model_config.tokenizer_mode,
        trust_remote_code=self.model_config.trust_remote_code,
        revision=self.model_config.tokenizer_revision,
    )
    
    # 配置特殊 token
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
        
    # 检查并处理特殊 token ID
    if (self.model_config.chat_template is not None and
            self.model_config.chat_template != "none"):
        tokenizer.chat_template = self.model_config.chat_template
        
    return tokenizer
3.3.3 Scheduler 初始化

Scheduler 负责调度请求,决定哪些请求应该被处理以及何时处理:

def _init_schedulers(self) -> List[Scheduler]:
    """初始化调度器"""
    # 创建多个调度器实例,每个对应一个模型副本
    schedulers = []
    for rank in range(self.num_nodes):
        # 创建调度器实例
        scheduler = Scheduler(
            # 配置调度器
            scheduler_config=self.scheduler_config,
            max_num_batched_tokens=self.worker_pool.max_num_batched_tokens,
            max_num_seqs=self.worker_pool.max_num_seqs,
            device=f"cuda:{
     
     self.parallel_config.local_rank}" if torch.cuda.is_available() else "cpu",
        )
        schedulers.append(scheduler)
    return schedulers
3.3.4 Worker 池初始化

Worker 池负责管理多个 Worker 实例,处理并发请求:

class WorkerPool:
    def __init__(
        self,
        model_config: ModelConfig,
        parallel_config: ParallelConfig,
        scheduler_config: SchedulerConfig,
        device_config: DeviceConfig,
        local_rank: int,
    ) -> None:
        # 1. 设置基本配置
        self.model_config = model_config
        self.parallel_config = parallel_config
        self.scheduler_config = scheduler_config
        self.device_config = device_config
        self.local_rank = local_rank
        
        # 2. 初始化其他参数
        self.max_num_seqs = scheduler_config.max_num_seqs
        self.max_num_batched_tokens = scheduler_config.max_num_batched_tokens
        
        # 3. 初始化 Worker
        self.workers: List[Worker] = []
        self.model_replicas = []
        
        # 4. 加载模型并创建 Worker
        self._initialize_workers()

Worker 初始化时会加载模型并准备执行环境:

def _initialize_workers(self) -> None:
    """初始化 Worker"""
    # 创建模型副本
    self.model_replicas = self._initialize_model_replicas()
    
    # 为每个模型副本创建一个 Worker
    for i, model_replica in enumerate(self.model_replicas):
        worker = Worker(
            model_config=self.model_config,
            model_replica=model_replica,
            # ... 其他参数
        )
        self.workers.append(worker)

3.4 推理阶段详细流程

3.4.1 请求接收与处理

用户通过 API 发送请求,LLMEngine 接收并处理请求:

def add_request(
    self,
    request_id: str,
    prompt: Optional[str],
    sampling_params: SamplingParams,
    prompt_token_ids: Optional[List[int]] = None,
    arrival_time: Optional[float] = None,
) -> Optional[int]:
    """添加一个新请求"""
    # 1. 记录请求到达时间
    if arrival_time is None:
        arrival_time = time.time()
    
    # 2. 处理输入
    if prompt is None and prompt_token_ids is None:
        raise ValueError("Either prompt or prompt_token_ids must be provided")
    
    # 3. 如果需要,使用 tokenizer 处理输入文本
    if prompt_token_ids is None:
        assert prompt is not None
        prompt_token_ids = self.tokenizer.encode(prompt)
    
    # 4. 创建请求并添加到队列
    request = Request(
        request_id=request_id,
        prompt=prompt,
        prompt_token_ids=prompt_token_ids,
        sampling_params=sampling_params,
        arrival_time=arrival_time,
    )
    
    # 5. 将请求添加到请求跟踪器
    self.request_tracker.add_request(request)
    
    # 6. 将请求添加到调度器
    self.schedulers[0].add_request(request)
    
    return len(prompt_token_ids)
3.4.2 请求调度

Scheduler 负责决定何时处理哪些请求:

def schedule(self) -> List[SequenceGroupMetadata]:
    """调度请求执行"""
    # 1. 获取系统状态
    now = time.time()
    
    # 2. 从等待队列和运行中队列选择可处理的序列组
    seq_groups = self._get_schedulable_groups()
    
    # 3. 根据调度策略对序列组排序
    seq_groups = self._sort_by_strategy(seq_groups)
    
    # 4. 根据资源约束选择可处理的序列组
    scheduled_groups = self._select_best_fitting_groups(seq_groups)
    
    # 5. 生成调度元数据
    metadata = self._create_schedule_metadata(scheduled_groups)
    
    return metadata
3.4.3 模型执行

ModelRunner 负责执行模型的前向传播:

def execute_model(
    self,
    input_ids: torch.Tensor,
    positions: torch.Tensor,
    kv_caches: List[torch.Tensor],
    block_tables: torch.Tensor,
) -> Tuple[torch.Tensor, List[torch.Tensor]]:
    """执行模型前向传播"""
    # 1. 准备输入
    batch_size = input_ids.shape[0]
    
    # 2. 如果已启用 CUDA 图捕获并且批大小匹配,使用 CUDA 图执行
    if (self.model_captured and batch_size in self.graph_runners and
            not self.disable_graphs):
        # 使用预先捕获的 CUDA 图执行模型
        return self._execute_with_graph(batch_size, input_ids, positions,
                                       kv_caches, block_tables)
    else:
        # 使用标准方式执行模型
        return self._forward_helper(input_ids, positions, kv_caches,
                                    block_tables)
3.4.4 KV 缓存管理

BlockManager 负责管理 KV 缓存的物理块:

def allocate(self, seq_id: int, num_tokens: int) -> List[int]:
    """为序列分配 KV 缓存块"""
    # 1. 计算需要的块数
    num_blocks = (num_tokens + self.block_size - 1) // self.block_size
    
    # 2. 分配物理块
    block_indices = []
    for _ in range(num_blocks):
        block_idx = self.block_allocator.allocate()
        block_indices.append(block_idx)
    
    # 3. 更新映射表
    self.block_tables[seq_id] = block_indices
    
    return block_indices
3.4.5 结果处理与返回

LLMEngine 处理模型输出并返回生成结果:

def _process_model_outputs(
    self,
    seq_group: SequenceGroup,
    outputs: ModelOutput,
    metadata: SequenceGroupMetadata,
) -> None:
    """处理模型输出"""
    # 1. 处理 logits 和采样结果
    logprobs = outputs.logprobs
    next_tokens = outputs.next_tokens
    
    # 2. 为每个序列更新状态
    for seq_id, seq in seq_group.seqs.items():
        # 获取对应的 token 和概率
        next_token = next_tokens[seq_id]
        logprob = logprobs[seq_id] if logprobs is not None else None
        
        # 更新序列状态
        seq.append_token_id(next_token, logprob)
        
        # 检查是否达到终止条件
        if self._is_finished(seq, next_token):
            seq.status = SequenceStatus.FINISHED

3.5 完整调用链路示例

以下是从用户请求到生成文本的完整调用链路示例:

  1. 用户 API 调用

    from vllm import LLM, SamplingParams
    
    # 设置采样参数
    sampling_params = SamplingParams(
        temperature=0.8,
        top_p=0.95,
        max_tokens=100
    )
    
    # 初始化 LLM
    llm = LLM(model="gpt-3.5-turbo")
    
    # 发送请求
    outputs = llm.generate("Tell me a joke", sampling_params=sampling_params)
    
    # 获取结果
    print(outputs[0].outputs[0].text)
    
  2. LLM 初始化

    # LLM 类内部初始化 LLMEngine
    self.engine = LLMEngine(
        model=model,
        tokenizer=tokenizer,
        # 其他参数...
    )
    
  3. 请求处理

    # LLM.generate 方法内部调用 LLMEngine.add_request
    request_id = self.engine.add_request(
        prompt=prompt,
        sampling_params=sampling_params
    )
    
  4. 请求调度与执行

    # LLMEngine 内部的调度循环
    while not engine_stopped:
        # 1. 调度请求
        scheduled_groups = self.scheduler.schedule()
        
        # 2. 将调度的请求发送给 Worker
        for worker, metadata in zip(self.workers, scheduled_groups):
            worker.execute_model(metadata)
        
        # 3. 处理模型输出
        for output in outputs:
            self._process_model_outputs(output)
        
        # 4. 更新请求状态
        self.request_tracker.update_status()
    
  5. 结果返回

    # LLMEngine 将结果返回给 LLM
    finished_requests = self.request_tracker.get_finished()
    
    # LLM 将结果返回给用户
    return [r.get_result() for r in finished_requests]
    

3.6 关键调用路径总结

vLLM 的核心调用流程可以总结为以下几个关键路径:

  1. 初始化路径

    LLM.__init__ → LLMEngine.__init__ → 
    [ModelConfig, DeviceConfig, ParallelConfig 初始化] → 
    Tokenizer 初始化 → Worker 池初始化 → 
    [模型加载, Scheduler 初始化] → 引擎线程启动
    
  2. 请求处理路径

    LLM.generate → LLMEngine.add_request → 
    Tokenizer.encode → RequestTracker.add_request → 
    Scheduler.add_request → Scheduler.schedule →
    Worker.execute_model → ModelRunner.forward → 
    [BlockManager 管理 KV 缓存] →
    LLMEngine._process_model_outputs → 
    LLM 返回结果
    
  3. 并行执行路径

    多个 Worker 并行执行 →
    每个 Worker 内部使用 CUDA 图加速 →
    使用张量并行和流水线并行提高吞吐量
    

通过这种模块化的设计,vLLM 能够高效地处理大量并发请求,同时优化内存使用和计算资源分配,为大型语言模型提供高性能的推理服务。

4. vLLM 关键工作机制

vLLM 的高性能源于几项关键技术创新。这些机制共同解决了大语言模型推理中的主要瓶颈问题:内存效率、计算速度和资源利用率。以下是这些核心机制的详细分析,即使没有深厚的技术背景,也能理解它们解决的问题和带来的好处。

5.1 PagedAttention 机制

PagedAttention 是 vLLM 的核心创新,通过分页管理 KV 缓存提高内存效率。这一机制受操作系统中的虚拟内存管理启发,解决了大语言模型推理中的关键瓶颈问题。

KV 缓存的挑战

在标准的 Transformer 解码器中,每生成一个新 token,都需要计算注意力,这涉及与之前所有 token 的 key 和 value 的交互。随着序列长度增加,这些 key 和 value 缓存(即 KV 缓存)占用的内存也线性增长:

# 传统 KV 缓存的内存占用
memory_per_seq = seq_len * num_layers * 2 * hidden_dim * dtype_size

对于长序列和批处理请求,内存消耗非常显著。例如,一个批次中有多个不同长度的序列,传统方法需要为每个序列分配足够大的连续内存块,导致内存碎片和浪费。

PagedAttention 原理

PagedAttention 的核心思想是将 KV 缓存组织成固定大小的物理块,并使用块表映射逻辑位置到物理块:

class PagedAttention:
    def __init__(self, block_size: int):
        self.block_size = block_size
        self.block_tables = {
   
   }  # 序列到物理块的映射
        self.physical_blocks = []  # 实际存储块
        
    def allocate_block(self, seq_id: int):
        """为序列分配新块"""
        if seq_id not in self.block_tables:
            self.block_tables[seq_id] = []
            
        # 分配新物理块
        block_id = self._find_free_block()
        self.block_tables[seq_id].append(block_id)
        
    def get_kv_cache(self, seq_id: int, positions: torch.Tensor):
        """获取缓存内容"""
        if seq_id not in self.block_tables:
            return None
            
        # 计算物理块索引和块内位置
        block_indices, block_offsets = self._compute_indices(positions)
        physical_blocks = [self.block_tables[seq_id][idx] for idx in block_indices]
        
        # 获取缓存内容
        return self._gather_blocks(physical_blocks, block_offsets)

工作流程:

  1. 内存分块:KV 缓存被划分为固定大小的块(如每块 16 个 token)
  2. 动态分配:每个序列动态分配所需的块,而不是预先分配
  3. 虚拟寻址:使用块表将逻辑位置映射到物理块
  4. 资源复用:当序列完成时,其块可以被释放并重用
技术细节

实际实现中,PagedAttention 包含以下关键组件:

  1. BlockAllocator:管理物理块的分配和释放
class BlockAllocator:
    def __init__(self, num_blocks: int):
        self.num_blocks = num_blocks
        self.free_blocks = set(range(num_blocks))
        self.used_blocks = set()
        
    def allocate(self) -> int:
        """分配一个空闲块"""
        if not self.free_blocks:
            raise RuntimeError("No free blocks available")
        
        block_id = next(iter(self.free_blocks))
        self.free_blocks.remove(block_id)
        self.used_blocks.add(block_id)
        return block_id
        
    def free(self, block_id: int) -> None:
        """释放一个块"""
        self.used_blocks.remove(block_id)
        self.free_blocks.add(block_id)
  1. BlockTable:维护序列到物理块的映射
class BlockTable:
    def __init__(self, block_size: int):
        self.block_size = block_size
        self.tables = {
   
   }  # seq_id -> List[block_id]
        
    def add_block(self, seq_id: int, block_id: int) -> None:
        """为序列添加一个块"""
        if seq_id not in self.tables:
            self.tables[seq_id] = []
        self.tables[seq_id].append(block_id)
        
    def get_physical_blocks(self, seq_id: int, positions: torch.Tensor) -> Tuple[List[int], torch.Tensor]:
        """获取物理块 ID 和偏移量"""
        block_ids = self.tables[seq_id]
        block_indices = positions // self.block_size
        block_offsets = positions % self.block_size
        physical_blocks = [block_ids[idx.item()] for idx in block_indices]
        return physical_blocks, block_offsets
  1. 注意力计算核心:高效实现分块注意力计算
# CUDA 核心伪代码
@cuda.kernel
def paged_attention_kernel(
    q: Tensor,             # [batch_size, num_heads, head_dim]
    k_cache: Tensor,       # [num_blocks, block_size, num_heads, head_dim]
    v_cache: Tensor,       # [num_blocks, block_size, num_heads, head_dim]
    block_tables: Tensor,  # [batch_size, max_blocks]
    output: Tensor         # [batch_size, num_heads, head_dim]
):
    # 获取线程索引
    batch_idx = cuda.blockIdx.x
    head_idx = cuda.blockIdx.y
    
    # 获取该序列的块表
    seq_blocks = block_tables[batch_idx]
    
    # 本地计算缓冲区
    local_k = shared_memory[...]
    local_v = shared_memory[...]
    
    # 加载查询向量
    query = q[batch_idx, head_idx]
    
    # 对每个块执行注意力计算
    for i in range(len(seq_blocks)):
        block_id = seq_blocks[i]
        
        # 加载 KV 缓存到共享内存
        local_k.copy_from(k_cache[block_id])
        local_v.copy_from(v_cache[block_id])
        
        # 计算注意力分数
        scores = compute_attention(query, local_k)
        
        # 应用注意力权重
        output[batch_idx, head_idx] += apply_attention(scores, local_v)
性能优势

PagedAttention 带来了多项显著优势:

  1. 内存效率:通过分块和动态分配,显著减少内存碎片

    • 传统方法:为每个序列分配最大长度的连续内存
    • PagedAttention:按需分配块,高效共享物理内存
  2. 批处理吞吐量:支持更多并发请求

    • 实验表明,相同硬件上,vLLM 可以处理比其他框架多 2-4 倍的并发请求
  3. 长文本支持:优雅处理长序列生成

    • 只需添加新块,无需重新分配或复制整个缓存
  4. 内存利用率:内存使用量与实际 token 数成正比,而非预分配

    • 典型场景中,与传统方法相比,内存使用可减少 50-70%
实际应用案例

在 vLLM 中,PagedAttention 的应用可以在多方面观察到:

  1. 动态批处理:高效支持连续批处理(Continuous Batching)

    # 在每次前向传播中添加新序列
    def step(self):
        # 调度新序列和正在进行的序列
        scheduled_seq_groups = self.scheduler.schedule()
        if not scheduled_seq_groups:
            return
        
        # 准备批处理输入
        batch = self._prepare_batch(scheduled_seq_groups)
        
        # 为新序列分配 KV 缓存块
        for seq_id in batch.new_seq_ids:
            self.block_manager.allocate_blocks(seq_id, batch.prompt_lens[seq_id])
            
        # 执行模型前向传播
        outputs = self.model_runner.forward(batch.input_ids, batch.block_tables, ...)
    
  2. 高效内存管理:动态分配和回收物理块

    def free_finished_sequences(self):
        """释放已完成序列的内存"""
        for seq_id in self.finished_sequences:
            # 获取分配的块
            blocks = self.block_tables.get_blocks(seq_id)
            
            # 释放块
            for block_id in blocks:
                self.block_allocator.free(block_id)
                
            # 移除块表条目
            self.block_tables.remove(seq_id)
    
  3. 缓存共享:支持 fork 和复制操作

    def fork_sequence(self, src_seq_id: int, dst_seq_id: int, fork_pos: int
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值