找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1871

积分

0

好友

259

主题
发表于 7 天前 | 查看: 19| 回复: 0

在探讨sglang这一高效推理框架时,ModelRunner作为其核心执行引擎,承担着模型加载与推理调度的关键职责。此前我们分析了调度器的工作机制,本文将聚焦于ModelRunner,深入解读其设计原理、代码实现及性能优化技巧。

ModelRunner的核心职责

ModelRunner类负责在分布式环境中管理和执行模型的每次生成。其核心功能是:接收一个批次的推理请求(ForwardBatch),在GPU上高效执行模型前向计算,并为后续采样步骤生成logits。我们可以将其职责分解为以下几个关键部分:

1. 环境与分布式设置 (__init__, init_torch_distributed)

  • 初始化分布式环境:根据传入的张量并行(TP)、流水线并行(PP)大小,使用torch.distributed初始化进程组。它支持多种后端(nccl, gloo等)以适应不同硬件(CUDA, ROCm/HIP, CPU)。
  • 设置并行策略:调用initialize_model_parallelinitialize_dp_attention等函数,为模型并行和特定的注意力机制并行(如数据并行注意力)配置通信组。
  • 处理配置参数:接收并存储来自ServerArgs的大量配置,如模型路径、数据类型、内存限制等,并进行模型特定的调整(model_specific_adjustment)。

2. 模型加载与配置 (load_model, model_specific_adjustment)

  • 动态加载模型:使用get_model工厂函数根据model_configload_config加载指定的模型架构(如Llama、Qwen)。支持多种权重格式,包括gguf
  • 模型特定优化:在加载前,model_specific_adjustment方法会根据模型架构、硬件(如Hopper GPU、AMX)和配置自动选择最优的注意力后端(attention_backend)。例如,在Hopper GPU上为Llama类模型默认启用FA3(FlashAttention-3)。
  • 在线权重更新:提供了update_weights_from_diskupdate_weights_from_distributedupdate_weights_from_tensor等方法,支持在服务运行时动态更新模型权重,这对于强化学习(RLHF)或持续学习场景至关重要。

3. 内存管理(KV缓存)(init_memory_pool)

  • 性能分析与内存规划profile_max_num_token方法会根据模型大小、可用GPU内存和静态内存比例(mem_fraction_static)估算出最大能容纳的token数量。这是保证服务稳定运行、防止OOM(Out of Memory)的关键一步。
  • 创建KV缓存池:基于估算出的token数量,初始化KV缓存。这是LLM推理性能优化的核心。
    • 它支持PagedAttention机制(当page_size > 1时),将KV缓存分成固定大小的“页”,通过逻辑块表进行管理,极大地减少了内存碎片,提高了内存利用率。
    • page_size=1时,相当于每个token的KV Cache都占用一个page,在块表中细粒度逐token分配。
  • 支持多种缓存结构:如标准的MHATokenToKVPoolMLATokenToKVPoolHybridLinearKVPool
  • 分配器 (TokenToKVPoolAllocator):创建并管理一个分配器,负责为每个请求的token序列在KV缓存池中分配和释放空间。

4. 高性能计算核与后端 (init_attention_backend, init_cuda_graphs)

  • 注意力后端选择:通过attn_backend属性,将底层的注意力计算实现与模型逻辑解耦。init_attention_backend根据配置选择并初始化具体的后端。
  • 多后端支持:支持多种业界领先的高性能注意力实现:flashinfertritonfa3aiterintel_amx
  • CUDA图 (CudaGraphRunner):对于decoding阶段这种计算图结构固定的操作,它会预先捕获整个计算流程为CUDA图。在后续的解码步骤中,直接重放这个图,可以显著减少CPU到GPU的调度开销和kernel launch延迟,是提升小批量解码速度的关键技术。

5. 前向传播执行 (forward)

  • 统一的入口forward方法是执行模型计算的统一入口。
  • 区分前向传播模式
    • is_extend():用于prefill阶段。
    • is_decode():用于decoding阶段。
    • is_idle():可能用于空闲状态,保持流水线运行。
  • CUDA图调度:在调用具体的前向传播函数前,会检查当前批次是否满足使用CUDA图的条件(can_run_cuda_graph)。如果满足,则通过cuda_graph_runner.replay执行,否则执行常规的PyTorch模块调用。
  • MoE支持:通过get_global_expert_distribution_recorder()记录MoE(Mixture-of-Experts)模型中专家(expert)的路由分布,这对于负载均衡和性能分析非常重要。EPLBManager(Expert Placement and Load Balancing Manager)可能会基于这些信息动态调整专家的位置。

6. 采样与后处理 (sample)

  • logits处理:在采样前,_preprocess_logits会应用logit_bias(例如,用于强制或禁止生成某些token)。
  • 约束性采样:它与SamplingBatchInfo交互,支持基于正则表达式或语法的采样(通过update_regex_vocab_mask),这对于结构化输出非常有用。
  • 进行采样:最后,调用self.sampler对象,根据指定的采样参数(如温度、top-p、top-k)从处理后的logits中采样出下一个token。

self.model的初始化与加载

self.model主要在ModelRunner的初始化流程中,通过调用self.load_model()方法来完成加载。这个过程可以分解为以下几个关键步骤:

  • 配置驱动:加载什么模型、以什么格式加载、加载到哪个设备上,都由配置对象(ModelConfig)决定。
  • 工厂模式:使用get_model这个工厂函数来根据配置创建正确的模型对象实例。
  • 加载器模式:使用get_model_loader来获取一个专门的加载器,这个加载器负责处理具体的文件格式(如Safetensors、PyTorch Bin)和权重加载逻辑。

详细步骤分解

1. 初始化入口 (ModelRunner.__init__)

当你创建一个ModelRunner实例时,它的构造函数__init__会调用self.initialize()

class ModelRunner:
    def __init__(...):
        # ... 其他初始化 ...
        self.initialize(min_per_gpu_memory)

2. 核心初始化 (initialize)

initialize()方法负责协调模型加载、内存池初始化等一系列准备工作。其中,它直接调用了self.load_model()

def initialize(self, min_per_gpu_memory: float):
        # ...
        self.load_model()
        # ...
        # 加载后还会进行一些后处理,如应用量化、张量并行等
        apply_torchao_config_to_model(...)
        self.apply_torch_tp()
        self.init_lora_manager()
        # ...

3. 模型加载核心 (load_model)

这是self.model被真正赋值的地方。我们来逐行分析这个方法的逻辑:

def load_model(self):
        # 1. 准备加载配置
        # 创建一个 LoadConfig 对象,它封装了加载格式、下载目录等信息。
        self.load_config = LoadConfig(
            load_format=self.server_args.load_format,
            download_dir=self.server_args.download_dir,
            model_loader_extra_config=self.server_args.model_loader_extra_config,
        )
        # 2. 兼容性处理 (Monkey Patching)
        # SGLang 为了支持某些功能(如 GGUF 格式或特定的量化层),
        # 会临时修改(“猴子补丁”)vLLM 等库的内部行为。
        monkey_patch_vllm_parallel_state()
        monkey_patch_isinstance_for_vllm_base_layer()
        # 3. 【核心调用】使用工厂函数 get_model
        # 这是一个关键步骤。get_model 函数会根据传入的配置,
        # 决定实例化哪个具体的模型类(例如 SGLang 版本的 Llama, Mixtral 等),
        # 并使用合适的加载器将权重填充进去。
        with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_WEIGHTS):
            self.model = get_model(
                model_config=self.model_config,  # 定义模型架构 (层数, 头数等)
                load_config=self.load_config,    # 定义加载格式和路径
                device_config=DeviceConfig(self.device),  # 定义目标设备
            )
        # 4. 恢复猴子补丁
        monkey_patch_vllm_parallel_state(reverse=True)
        monkey_patch_isinstance_for_vllm_base_layer(reverse=True)
        # 5. 加载额外参数
        # 例如,如果使用 FP8 KV Cache,这里会加载对应的缩放因子。
        if self.server_args.kv_cache_dtype == "fp8_e4m3":
            if callable(getattr(self.model, "load_kv_cache_scales", None)):
                self.model.load_kv_cache_scales(...)
        # ...

4. get_modelget_model_loader的内部工作

  1. get_model接收model_configload_config

    • 根据model_config.model_pathhf_config中的模型类型(如“LlamaForCausalLM”),从一个注册表中找到SGLang自己的、优化过的模型类。
    • 实例化这个模型类,此时模型结构已经建好,但权重都是空的。
    • 调用get_model_loader(load_config)来获取一个加载器实例。
    • 调用加载器的load_weights方法,把模型实例和权重文件路径/配置传给它,由它完成权重的填充。
    • 返回填充好权重的模型实例。
  2. DefaultModelLoader负责处理具体的加载细节。

    • 它会根据load_format(如‘huggingface-safetensors’)确定如何解析权重文件。
    • 它会迭代权重文件中的每一个张量(tensor)。
    • 对于每个张量,它会根据张量并行(Tensor Parallelism)的配置(tp_sizetp_rank)决定当前GPU rank应该加载完整张量还是张量的一个分片(shard)。
    • 最后,它使用default_weight_loader将张量数据加载到模型对应参数的内存中,并确保数据在正确的设备上(如cuda:0)。

总结self.model的加载是一个精心设计的流程,通过配置驱动和工厂模式,实现了灵活、高效的模型初始化,为后续的人工智能推理任务奠定基础。

min_per_gpu_memory的作用

简单来说,它的核心作用是:在多GPU(张量并行)环境中,获取所有参与计算的GPU中可用显存的最小值,并以此为基准来统一分配关键的动态资源(主要是KV Cache),从而防止因各GPU可用显存不一致而导致的“短板效应”和内存溢出(OOM)问题。

1. min_per_gpu_memory的诞生(在init_torch_distributed方法中)

这个变量在init_torch_distributed方法中被计算并返回。

def init_torch_distributed(self):
        # ... (省略了分布式环境初始化的代码) ...
        initialize_model_parallel(
            tensor_model_parallel_size=self.tp_size,
            # ...
        )
        # 关键代码行
        min_per_gpu_memory = get_available_gpu_memory(
            self.device,
            self.gpu_id,
            distributed=get_world_group().world_size > 1,  # 当分布式组>1时为True
            cpu_group=get_world_group().cpu_group,
        )
        self.tp_group = get_tp_group()
        # ...
        # 内存均衡性检查
        local_gpu_memory = get_available_gpu_memory(self.device, self.gpu_id)
        if self.tp_size > 1 and not self.is_draft_worker:
            if min_per_gpu_memory < local_gpu_memory * 0.9:
                # ...
                raise ValueError(
                    "The memory capacity is unbalanced. Some GPUs may be occupied by other processes."
                )
        # ...
        return min_per_gpu_memory

计算过程get_available_gpu_memory函数被调用。当distributed=True时,这个函数不仅会获取当前GPU(self.gpu_id)的可用显存,还会通过分布式通信操作(如torch.distributed.all_reduceMIN操作)与张量并行组中的其他所有GPU进行同步,最终返回所有GPU中可用显存的最小值。

为何是最小值?这就是著名的“木桶效应”。在张量并行中,所有GPU协同工作,处理一个模型的不同部分。如果资源(如KV Cache)的分配超出了任何一个GPU的承受能力,那么整个系统就会因为那块“最短的木板”(显存最小的GPU)而崩溃。因此,必须以最受限的那个GPU为标准。

均衡性检查:紧接着,代码会检查这个全局最小值min_per_gpu_memory是否远小于当前GPU的可用显存local_gpu_memory。如果差异过大(例如小于90%),就意味着GPU之间的负载或初始状态非常不均衡,这通常是个危险信号,所以会直接报错。

2. min_per_gpu_memory的传递和使用

min_per_gpu_memory__init__中被获取后,被传递给了后续的初始化流程。

class ModelRunner:
    def __init__(self, ...):
        # ...
        min_per_gpu_memory = self.init_torch_distributed()
        # ...
        self.initialize(min_per_gpu_memory)  # << 传递给 initialize
    def initialize(self, min_per_gpu_memory: float):
        # ...
        self.init_memory_pool(  # << 再次传递给 init_memory_pool
            min_per_gpu_memory,
            server_args.max_running_requests,
            server_args.max_total_tokens,
        )
        # ...
    def init_memory_pool(
        self,
        total_gpu_memory: int,  # << 在这里,min_per_gpu_memory 被命名为 total_gpu_memory
        max_num_reqs: Optional[int] = None,
        max_total_tokens: Optional[int] = None,
    ):
        # ...
        self.max_total_num_tokens = self.profile_max_num_token(total_gpu_memory)  # << 最终用于计算KV Cache大小
        # ...

可以看到,这个值经过层层传递,最终在init_memory_pool方法中作为total_gpu_memory参数,被用来调用profile_max_num_token

3. 最终目的:计算KV Cache的大小

profile_max_num_token方法是这个变量的最终归宿。

    def profile_max_num_token(self, total_gpu_memory: int):
        available_gpu_memory = get_available_gpu_memory(
            # ...
        )
        # ...
        cell_size = (
            # ... 计算一个token的KV Cache在所有层中占用的字节数
        )
        # 核心计算逻辑
        rest_memory = available_gpu_memory - total_gpu_memory * (
            1 - self.mem_fraction_static
        )
        max_num_token = int(rest_memory * (1 << 30) // cell_size)
        return max_num_token
  • 这里的total_gpu_memory就是我们一路追踪的min_per_gpu_memory
  • self.mem_fraction_static是一个配置参数,表示用于静态资源(如模型权重)的显存比例。
  • total_gpu_memory * (1 - self.mem_fraction_static)这部分计算出了理论上可以用于动态资源(主要是KV Cache)的显存大小。
  • rest_memory变量名有点误导,但它实际上代表了可用于KV Cache的总内存。
  • max_num_token:用这部分可用的内存除以每个token的KV Cache大小(cell_size),就得到了系统能容纳的最大token数量,也就是KV Cache池的大小。

总结min_per_gpu_memory的作用链条如下:

  1. 同步:在分布式初始化时,通过all_reduce操作找到所有TP-rank的GPU中可用显存的最小值。
  2. 检查:确认各GPU间的可用显存没有严重不平衡。
  3. 计算:将这个最小值作为整个系统的可用显存基准。
  4. 分配:根据这个基准和配置参数(mem_fraction_static)计算出能分配给KV Cache的总内存,并最终确定KV Cache池的大小(max_total_num_tokens)。

通过这种方式,SGLang确保了即使在不同GPU可用显存有差异的情况下,所分配的KV Cache大小对所有GPU来说都是安全的,避免了某个GPU因内存不足而成为系统瓶颈,从而保证了整个多GPU推理服务的稳定运行。

forward函数解析

forward函数是ModelRunner对外提供的、执行一次完整前向传播的统一入口。它的核心作用是:接收一个批次的请求,根据forward_batch中的forward_mode,分发调用对应阶段的模型forward(prefill或decoding),并返回next token的logits。

def forward(
    self,
    forward_batch: ForwardBatch,
    skip_attn_backend_init: bool = False,
    pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> Tuple[Union[LogitsProcessorOutput, PPProxyTensors], bool]:

输入参数分析

  • forward_batch: ForwardBatch:它是一个封装了该批次所有必要信息的复杂数据结构。它包含:
    • input_idspositions:模型计算所需的输入token和位置。
    • forward_mode:关键标志,指明是prefill(extend)还是decoding(decode)。
    • sampling_info:采样参数,如温度、top_p等。
    • KV缓存相关的元数据指针,如out_cache_locreq_pool_indices。前者指的是当前要处理的token对应的KV缓存在显存的写入位置,后者为请求池映射索引。
  • skip_attn_backend_init: bool = False:一个性能优化标志。在某些复杂场景(如推测解码的验证步骤)下,注意力后端所需的元数据可能已经在前一步设置好了,此时可以跳过重复的初始化,节省开销。
  • pp_proxy_tensors: Optional[PPProxyTensors] = None:这个参数用于流水线并行(Pipeline Parallelism, PP)。
    • 如果当前ModelRunner是流水线的第一个阶段(pp_rank == 0),这个参数为None
    • 如果它处于中间或最后一个阶段,pp_proxy_tensors就包含了从上一个阶段的GPU传递过来的隐藏状态(hidden states)。

返回值Tuple[Union[LogitsProcessorOutput, PPProxyTensors], bool],返回一个元组,包含两个值:

  1. 计算结果:如果是流水线的最后一个阶段,它返回LogitsProcessorOutput,其中包含了最终的logits,可以直接用于采样;如果它不是最后一个阶段,它返回PPProxyTensors,其中包含了计算出的中间隐藏状态,将传递给流水线的下一个阶段。
  2. 执行状态:一个布尔值,指示本次前向传播是否成功地通过CUDA Graph执行。这对于上层调用者(如服务器主循环)进行性能统计和日志记录非常有用。

forward函数的分布记录

with get_global_expert_distribution_recorder().with_forward_pass(
    self.forward_pass_id,
    forward_batch,
):
    output = self._forward_raw(
        forward_batch, skip_attn_backend_init, pp_proxy_tensors
    )

get_global_expert_distribution_recorder():获取一个全局的MoE专家分布记录器实例。当模型(特别是MoE层)执行时,它会与记录器交互,将专家的选择信息记录下来。记录器的作用在下面的代码中体现:

if self.eplb_manager is not None:
    self.eplb_manager.on_forward_pass_end()
  • eplb_manager是“Expert Placement and Load Balancing Manager”(专家放置与负载均衡管理器)的缩写。
  • 在完成一次前向传播(并且相关的专家使用数据已经被记录)后,代码会通知EPLB管理器。
  • 为什么:EPLB管理器可以利用刚刚收集到的最新专家使用数据,分析是否存在负载不均衡(某些专家的使用频率远高于其他专家)。如果检测到不均衡,它可能会触发一个后台任务,来动态地重新分配专家在不同GPU上的位置,以优化未来的性能。

_forward_raw函数解析

_forward_raw函数是forward函数的核心实现,主要负责根据forward_batch中的forward_mode,分发调用对应阶段的模型forward(prefill或decoding),并返回next token的logits。

def _forward_raw(
    self,
    forward_batch: ForwardBatch,
    skip_attn_backend_init: bool,
    pp_proxy_tensors: Optional[PPProxyTensors],
) -> Tuple[Union[LogitsProcessorOutput, PPProxyTensors], bool]:
  • 输入参数:与forward函数几乎相同,它接收包含了所有请求信息的forward_batch,以及用于流水线并行的pp_proxy_tensors
  • 返回值:它返回一个元组(计算结果, 是否使用了CUDA图)。这个返回值会直接透传给调用它的forward函数。

接下来,_forward_raw会判断当前推理阶段是否在cuda graph中。如果满足条件,则通过cuda_graph_runner.replay执行,否则执行常规的PyTorch模块调用。

can_run_cuda_graph = bool(
    forward_batch.forward_mode.is_cuda_graph()
    and self.cuda_graph_runner
    and self.cuda_graph_runner.can_run(forward_batch)
)

它检查三个条件,必须全部满足才能使用CUDA Graph:

  1. forward_batch.forward_mode.is_cuda_graph():判断当前批次适合用CUDA Graph执行。这通常意味着它是一个解码(decode)批次,并且批次大小配置等符合之前capture CUDA Graph时的条件。
  2. self.cuda_graph_runnerModelRunner已经成功初始化了CUDA Graph运行器。如果CUDA Graph被禁用或初始化失败,这里就是None
  3. self.cuda_graph_runner.can_run(forward_batch):运行器本身再次确认当前批次的具体参数(如批次大小)与它所拥有的预捕捉图完全匹配。

can_run_cuda_graph变量缓存了这个检查结果,因为它不仅用于决定分支,还会作为函数的返回值返回。

if can_run_cuda_graph:
    ret = self.cuda_graph_runner.replay(
        forward_batch,
        skip_attn_backend_init=skip_attn_backend_init,
        pp_proxy_tensors=pp_proxy_tensors,
    )

如果满足条件,则通过cuda_graph_runner.replay执行,否则执行常规的PyTorch模块调用。调用cuda_graph_runner.replay方法几乎没有CPU开销,它直接向GPU提交一个预先录制好的、完整的计算图,极大地减少了内核启动(kernel launch)的延迟,是提升批量解码吞吐量的关键。

如果不是cuda graph,则需要检查是否为decode,调用常规decode生成;否则调用prefill生成。

# 3. 如果不能用CUDA Graph,则检查是否为decode,调用常规decode生成
elif forward_batch.forward_mode.is_decode():
    ret = self.forward_decode(forward_batch, pp_proxy_tensors=pp_proxy_tensors)
# 4. 如果都不是上面两种情况,则执行prefill
elif forward_batch.forward_mode.is_extend():
    ret = self.forward_extend(
        forward_batch,
        skip_attn_backend_init=skip_attn_backend_init,
        pp_proxy_tensors=pp_proxy_tensors,
    )

forward_extend函数分析

forward_extend函数专门负责处理预填充(Prefill)阶段。这是LLM推理的第一步,模型需要一次性处理用户输入的整个prompt,计算出所有token的KV缓存,并生成第一个新token的logits。

关键职责分解

1. 初始化注意力后端元数据

if not skip_attn_backend_init:
    self.attn_backend.init_forward_metadata(forward_batch)
  • 做什么?:调用self.attn_backend(例如FlashInfer、Triton等高性能注意力实现)的init_forward_metadata方法。
  • 为什么重要?:这是性能优化的核心。现代注意力内核为了达到极致的速度,需要预先计算“元数据”来理解如何高效地访问和计算。这些元数据可能包括序列长度的累积和、请求池映射索引等。init_forward_metadata就是根据forward_batch中的信息来准备好这些元数据。
  • skip_attn_backend_init:这个标志提供了一个优化的可能性。在某些高级场景下(如推测解码的验证阶段),元数据可能已经被前一步计算好了,此时可以跳过这个步骤,节省CPU开销。

2. 执行模型的forward

return self.model.forward(
    forward_batch.input_ids,
    forward_batch.positions,
    forward_batch,
    **kwargs,
)
  • 做什么?:调用实际模型(如Llama、Qwen)的forward方法,执行计算。
  • 传递的参数分析
    • forward_batch.input_ids:必需的参数,输入的token ID序列。
    • forward_batch.positions:必需的参数,每个token的位置ID,对于使用旋转位置编码(RoPE)等技术的模型至关重要。
    • forward_batch:直接将整个forward_batch上下文对象传递下去。模型内部的各个层(尤其是注意力层)可以直接从这个对象中获取所需信息。这种设计简化了函数签名,并使得添加新功能更容易。
    • **kwargs:将前面准备好的所有可选参数解包并传递进去。

设计与工程洞察

  • 抽象与实现分离forward_extend不关心注意力计算的具体实现,只与self.attn_backend这个抽象接口交互。SGLang可以轻松切换或添加新的注意力后端,而无需修改此处的逻辑。
  • 上下文对象模式:将所有与批次相关的信息封装在ForwardBatch对象中,并将其作为单一参数传递,提高了代码的可读性和可维护性。

forward_decodeforward_extend的区别

forward_decode函数的实现及使用和forward_extend基本相同,主要差异在于注意力后端的处理模式。以使用flashinfer作为attn_backend为例:

  1. 设置不同的self.forward_metadata

    • Decode模式:设置DecodeMetadata,更新indices_updater_decode。它只需要更新每个请求最新的位置映射。
    • Extend模式:设置PrefillMetadata,更新indices_updater_prefill。它负责整段长序列输入,需要计算序列的起止偏移,并处理前缀与新输入的拼接逻辑。
  2. decode模式下可以进行CUDA Graph的优化和执行:decode阶段的计算图结构固定,适合预捕捉为CUDA图来提升性能。

因此,虽然都是调用self.model.forward,但在注意力端的decode和extend的实现差异很大,主要体现在元数据初始化和计算优化策略上。

通过以上解析,我们可以看到ModelRunner在sglang框架中的核心地位。它不仅管理模型的生命周期,还通过精细的内存管理和高性能后端优化,确保了推理服务的高效与稳定。对于希望深入理解大规模语言模型推理系统的开发者,这些设计细节提供了宝贵的参考。本文的分享旨在促进技术交流,更多相关内容可在云栈社区技术文档板块进一步探索。




上一篇:Kubernetes v1.35 补充组控制GA:Strict策略提升容器安全性
下一篇:pgAdmin 4 v9.11版本发布:开源免费vs商业全能,数据库管理工具如何选?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:18 , Processed in 0.196930 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表