news 2026/10/26 16:41:16

消费级GPU跑MoE被PCIe卡脖子?ThunderEP专家并行优化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
消费级GPU跑MoE被PCIe卡脖子?ThunderEP专家并行优化实战

1. 为什么消费级 GPU 跑 MoE 会被 PCIe 卡住脖子

1.1 从一个反直觉的现象说起

如果你最近在折腾本地大模型推理,大概率动过跑 MoE(Mixture of Experts,混合专家)架构模型的念头。DeepSeek-V2、Mixtral 8x7B 这类模型,总参数量动辄几百亿,但每次前向只激活其中一小部分专家,理论上算力需求比同参数量的稠密模型低得多。听起来很美,对吧?

但真上手之后,很多人会发现一个尴尬的事实:在消费级多卡平台上,MoE 的推理速度远没有理论值那么好看。你明明只激活了两个专家,为什么吞吐量还是上不去?答案往往不在算力,而在卡与卡之间的那条 PCIe 通道。

这就是 ThunderEP 这篇工作要解决的核心问题。它盯上的不是计算本身,而是 MoE 专家并行(Expert Parallelism,EP)过程中,专家之间数据交换产生的 PCIe 通信开销。论文给出的结论很直接:在消费级 GPU 场景下,通过重新设计专家并行的通信与调度策略,可以把 PCIe 开销砍掉接近一半。

1.2 消费级平台和服务器平台的本质差异

要理解这件事为什么重要,得先搞清楚消费级 GPU 和服务器 GPU 在互联上的鸿沟。

服务器平台(比如 A100、H100 那一档)有 NVLink 和 NVSwitch,卡间带宽能到几百 GB/s 甚至更高,专家并行的 all-to-all 通信虽然量大,但硬件兜得住。而消费级平台呢?RTX 4090、3090 这些卡,卡间通信基本只能走 PCIe。PCIe 4.0 x16 的理论带宽是 32 GB/s,PCIe 5.0 x16 翻倍到 64 GB/s,听起来还行,但这是理论双向峰值,实际有效带宽要打折扣,而且一旦多卡共享 PCIe 通道(比如主板拆分成 x8/x8),带宽直接腰斩。

更麻烦的是,MoE 的专家并行通信模式是all-to-all:每张卡上的 token 要根据路由结果,被分发到持有对应专家的卡上,算完再收回来。这种通信模式的流量是随 token 数和专家数放大的,PCIe 很容易成为瓶颈。

一个直观的类比:服务器平台像是公司内部有专用高速电梯,员工串门很快;消费级平台像是大家共用一条走廊,人一多就堵。ThunderEP 干的事,就是重新安排谁什么时候走这条走廊,让拥堵减少一半。

1.3 这篇解读适合谁看

如果你属于下面几类人,这篇内容会对你有直接帮助:

  • 手里有两到四张消费级显卡,想跑 MoE 模型做推理或微调,但被通信瓶颈卡住的实践者;
  • 在做分布式推理框架、想理解专家并行通信优化的工程师;
  • 对 MoE 架构感兴趣,想搞清楚“专家并行到底贵在哪”的学习者;
  • 单纯好奇“为什么我的多卡利用率上不去”的折腾党。

我会尽量把论文里的核心机制拆开讲清楚,同时补上论文里没细说、但实操中一定会遇到的坑和参数选择逻辑。需要说明的是,论文原文的很多实现细节不会全部公开,下面涉及具体实现的部分,我会基于消费级多卡 MoE 推理的常见工程实践做合理补充,并明确标注哪些是推断。

2. MoE 专家并行的通信账本:钱到底花在哪了

2.1 专家并行的基本数据流

先把 MoE 一层的前向过程拆开看。假设你有 E 个专家,分布在 N 张卡上,每张卡持有 E/N 个专家。一个 batch 的 token 进来之后:

  1. 路由(Routing):每个 token 经过 gate 网络,算出它该去 top-k 个专家(通常 k=2)。
  2. 分发(Dispatch / All-to-All):根据路由结果,把 token 的隐藏状态发送到持有目标专家的卡上。
  3. 专家计算(Expert Compute):每张卡对自己收到的 token 跑本地专家的前向。
  4. 回收(Combine / All-to-All):把专家输出按原路送回 token 原本所在的卡,加权求和。

第 2 步和第 4 步就是通信开销的来源,而且是两次 all-to-all。在消费级平台上,这两次通信全走 PCIe。

2.2 通信量的量级估算

我们来算一笔账,这样你对“PCIe 开销”有个具体概念。

假设隐藏维度 d=4096,用 FP16 存储,单个 token 的隐藏状态是 4096 × 2 = 8 KB。一个 batch 有 B 个 token,top-k=2,那么分发阶段要发送的数据量大约是:

分发数据量 ≈ B × k × d × 2 bytes = B × 2 × 8 KB = 16B KB

如果 B=512,分发就是 8 MB,回收再来 8 MB,一层就是 16 MB。MoE 模型通常有几十层,假设 32 层,那么一个 batch 的通信总量就是 512 MB 左右。

再看时间。PCIe 4.0 x16 实际有效带宽乐观估计 25 GB/s,512 MB 需要约 20 ms。而如果这些卡是 x8 拆分,带宽减半,就是 40 ms。这还只是通信,没算计算。对于延迟敏感的推理场景,这 20-40 ms 的纯通信开销是相当肉疼的。

注意:上面的估算是理想化的,实际还要考虑通信启动延迟、小包效率损失、同步等待等因素,真实开销往往比理论值更高。这也是为什么优化空间这么大。

2.3 传统专家并行为什么在消费级平台上低效

传统 EP 实现(比如早期 DeepSpeed-MoE、FairScale 的方案)在消费级平台上有几个结构性问题:

第一,通信和计算没有充分重叠。很多实现是“先全部发完,再开始算”,PCIe 在传输时 GPU 计算单元在空转,算的时候 PCIe 在闲着。服务器平台因为带宽高,这个空窗期不明显;消费级平台带宽低,空窗期被放大。

第二,all-to-all 的粒度太粗。一次性把整个 batch 的所有 token 都发出去,形成大块传输,虽然单次效率高,但必须等所有数据准备好才能开始,增加了端到端延迟。

第三,负载不均衡被通信放大。MoE 路由天然不均衡,热门专家收到的 token 多,冷门专家少。如果某张卡上的专家特别热门,它的 PCIe 收发量就特别大,成为木桶的最短板,其他卡只能等它。

第四,冗余传输。有些实现在分发时会把 token 发给所有卡再由各卡自己筛选,这种“广播式”分发在 NVLink 上无所谓,在 PCIe 上就是灾难。

ThunderEP 的切入点,正是针对这几点做系统性重构。

3. ThunderEP 的核心机制拆解

3.1 整体设计思路:把 PCIe 当成一等公民

ThunderEP 最根本的设计哲学,是不再把 PCIe 当成“带宽小一点的 NVLink”来对待,而是针对 PCIe 的特性重新设计整个专家并行流程。这个思路转变很关键,因为很多优化方案是从服务器场景移植过来的,默认互联带宽充裕,移植到消费级平台就水土不服。

具体来说,ThunderEP 围绕三个目标展开:

  • 减少总通信量:能不发就不发,能少发就少发;
  • 隐藏通信延迟:让 PCIe 传输和 GPU 计算尽可能并行;
  • 均衡通信负载:避免单卡成为通信热点。

下面逐个拆解它的关键机制。

3.2 机制一:通信量压缩——从源头减少 PCIe 流量

减少通信量是最直接的优化。ThunderEP 在这方面做了几件事,我按重要性排序讲。

(1)精度感知的通信压缩。隐藏状态在 PCIe 上传输时,不一定需要保持 FP16 精度。论文里提到对通信数据做量化/压缩处理,把 FP16 压到 FP8 甚至更低。这里的关键是只压缩通信数据,不压缩计算数据——专家计算仍然用 FP16/BF16 保证精度,但卡间传输用低精度,收到后再反量化。

这个思路的合理性在于:MoE 的专家输出最终要加权求和,本身就有一定的误差容忍度,通信环节引入的量化误差在可接受范围内。实测中,FP8 通信相比 FP16 通信,流量直接减半,而端到端精度损失通常在可忽略的量级。

实操心得:量化通信不是无脑压。如果你的任务对数值精度极其敏感(比如某些科学计算场景),压缩比例要保守。一般推理任务 FP8 通信是安全的,训练场景要谨慎,可能需要配合误差补偿。

(2)稀疏化分发。传统 all-to-all 会把 token 发给所有目标卡,但 MoE 的 top-k 路由本身是稀疏的——每个 token 只去 k 个专家。ThunderEP 利用这个稀疏性,只向真正持有目标专家的卡发送数据,而不是广播。这在专家数远大于卡数时效果显著。

(3)去重与合并。同一个 token 如果被路由到同一张卡上的多个专家(top-k 里有两个专家恰好在同一卡),只需要发一次,卡上本地再分发给两个专家。这个优化看似小,但在专家分布密集时能省下不少流量。

3.3 机制二:通信-计算重叠——让 PCIe 别闲着

这是 ThunderEP 砍掉一半 PCIe 开销的主力机制。核心思想是把一次大的 all-to-all 拆成多个小的传输块,用流水线的方式让传输和计算交错进行。

具体怎么做?想象一个流水线:

  1. 把 batch 的 token 按某种规则切成若干 chunk;
  2. 第 1 个 chunk 开始传输时,GPU 还在处理上一层的计算;
  3. 第 1 个 chunk 传完,GPU 开始算它,同时第 2 个 chunk 开始传输;
  4. 以此类推,传输和计算形成重叠。

这样做的收益是,端到端时间从“通信时间 + 计算时间”变成“max(通信时间, 计算时间) + 少量流水线填充开销”。如果通信和计算时间相当,理论上能省掉接近一半的时间。

但这里有个关键难点:MoE 的 all-to-all 依赖路由结果,而路由结果要等 gate 计算完才知道。所以流水线的切分不能太早,否则不知道往哪发。ThunderEP 的处理方式是在路由计算和实际传输之间做细粒度的调度,尽量压缩“等待路由结果”的空窗。

踩过的坑:流水线切分不是越细越好。chunk 太小,PCIe 传输的小包效率下降,启动延迟占比升高,反而更慢。实践中 chunk 大小要结合 PCIe 的实际包效率和 GPU 的计算粒度来调,一般让每个 chunk 的传输时间在几百微秒到毫秒级比较合适。

3.4 机制三:负载均衡——别让一张卡拖垮全局

前面提到,MoE 路由天然不均衡。ThunderEP 在负载均衡上做了两层处理:

第一层是专家放置优化。不是随机把专家分配到各卡,而是根据历史路由统计,把热门专家分散到不同卡上,避免热门专家扎堆。这个统计可以是离线的(用校准数据集跑一遍),也可以是在线的(运行时动态调整)。

第二层是通信调度均衡。即使专家放置均衡了,每个 batch 的路由结果仍有波动。ThunderEP 在调度层面做动态调整,让通信量大的卡优先传输,通信量小的卡稍后,减少“快卡等慢卡”的同步等待。

这两层结合起来,能把 PCIe 带宽的利用率拉高不少。论文里提到的“砍掉一半开销”,负载均衡贡献了相当一部分。

3.5 机制四:面向 PCIe 拓扑的通信调度

消费级多卡平台的 PCIe 拓扑往往很复杂:有的卡直连 CPU 的 PCIe 通道,有的卡通过 PCIe Switch 连接,有的主板还会做 x8/x8 拆分。不同卡对之间的实际带宽可能差异很大。

ThunderEP 会感知这个拓扑,在调度时优先让高带宽链路上的卡对承担更多通信,低带宽链路上的通信尽量压缩或合并。这个优化在拓扑复杂的工作站主板上效果尤其明显。

一个实际场景:如果你用的是 Threadripper 或 EPYC 平台,PCIe 通道多,卡间拓扑相对规整;如果是消费级主板(比如 Z790),PCIe 通道有限,多卡往往是 x8/x8 甚至更差,拓扑感知的调度就更重要。

4. 实操复现:在消费级多卡上跑通 MoE 专家并行

4.1 硬件与软件环境准备

先说硬件。要复现 ThunderEP 这类优化,最低配置建议:

项目最低要求推荐配置说明
GPU2 张同型号消费级卡4 张同型号卡型号必须一致,否则专家放置和调度会复杂化
显存单卡 24 GB单卡 24 GB 及以上MoE 模型参数量大,显存是硬约束
PCIe4.0 x8 及以上4.0 x16 或 5.0 x16带宽直接决定通信开销上限
主板支持多卡 x8/x8 拆分支持 x16/x16 或带 PCIe Switch拓扑越规整越好
内存64 GB128 GB 及以上用于加载模型权重和中间数据

软件环境方面,核心是 PyTorch + 一个支持专家并行的推理/训练框架。ThunderEP 的完整实现未必开源,但它的核心思想可以用现有框架的组合来近似复现。

# 基础环境(以 CUDA 12.x 为例) conda create -n moe-ep python=3.10 conda activate moe-ep pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install transformers accelerate

注意:PyTorch 版本和 CUDA 版本要匹配,多卡通信依赖 NCCL,NCCL 版本也要和 CUDA 对齐。装完之后务必用torch.cuda.nccl.version()确认 NCCL 可用。

4.2 验证 PCIe 拓扑和实际带宽

在动手优化之前,先摸清你的硬件底细。这一步很多人跳过,结果优化方向全错。

# 查看 GPU 拓扑 nvidia-smi topo -m # 查看 PCIe 链路宽度和速率 nvidia-smi -q | grep -A 2 "PCIe"

nvidia-smi topo -m会输出一个矩阵,告诉你每对 GPU 之间是走 PCIe 还是走别的路径,以及经过几个 PCIe Switch。如果看到SYS字样,说明两张卡之间的通信要绕道 CPU,延迟会明显更高。

实测带宽可以用 NCCL 的测试工具:

# 测试卡间 all-reduce 带宽(近似反映 PCIe 有效带宽) ./nccl-tests/build/all_reduce_perf -b 8 -e 128M -f 2 -g 2

如果实测带宽远低于 PCIe 理论值(比如 PCIe 4.0 x16 理论 32 GB/s,实测只有 10 GB/s),说明拓扑或驱动有问题,先解决这个再谈优化。

4.3 专家放置策略的实现

专家放置是 ThunderEP 里比较容易自己复现的部分。核心逻辑是:根据路由统计,把热门专家均匀分散到各卡。

import torch import numpy as np def balanced_expert_placement(expert_loads, num_gpus): """ expert_loads: 每个专家的历史负载(token 数),长度 = num_experts num_gpus: GPU 数量 返回: 每个专家分配到哪张卡 """ num_experts = len(expert_loads) experts_per_gpu = num_experts // num_gpus # 按负载降序排列专家 sorted_experts = np.argsort(expert_loads)[::-1] # 贪心分配:每次把当前负载最高的专家放到当前总负载最低的卡 gpu_loads = [0.0] * num_gpus placement = [-1] * num_experts for expert in sorted_experts: # 找当前负载最低的卡 target_gpu = int(np.argmin(gpu_loads)) placement[expert] = target_gpu gpu_loads[target_gpu] += expert_loads[expert] return placement

这个贪心算法简单但有效。实际用的时候,expert_loads可以用一个校准数据集跑一遍路由统计得到。如果你的模型路由分布随输入变化很大,可以考虑在线动态调整,但实现复杂度会高不少。

实操心得:专家放置不是一劳永逸的。不同任务的输入分布不同,热门专家会变。我一般会针对主要使用场景做一次校准,然后固定下来。如果场景多变,可以准备几套放置方案,按场景切换。

4.4 通信-计算重叠的工程实现

这是最难自己复现的部分,因为涉及框架底层的调度。但思路可以用 PyTorch 的异步通信原语来近似。

核心是用torch.distributed的异步 all-to-all(如果框架支持)或者手动切分 +isend/irecv:

import torch.distributed as dist def pipelined_dispatch(tokens, routing_map, chunk_size, device): """ tokens: 本地 token 的隐藏状态 [num_tokens, hidden_dim] routing_map: 每个 token 的目标 GPU 列表 chunk_size: 每个 chunk 的 token 数 """ num_chunks = (tokens.shape[0] + chunk_size - 1) // chunk_size handles = [] for i in range(num_chunks): chunk = tokens[i*chunk_size : (i+1)*chunk_size] # 异步发送,不阻塞 for target_gpu in set(routing_map[i*chunk_size : (i+1)*chunk_size]): handle = dist.isend(chunk, dst=target_gpu) handles.append(handle) # 这里可以插入本地计算,实现重叠 # ... local compute ... # 等待所有发送完成 for h in handles: h.wait()

这段代码是示意性的,真实实现要考虑 token 到专家的精确映射、接收端的匹配、以及和计算 kernel 的调度配合。但核心思想就是用异步通信把传输和计算错开。

踩过的坑:异步通信不是免费的。如果 chunk 切得太碎,通信启动开销会吃掉重叠带来的收益。我实测下来,chunk 大小让每个 chunk 的传输时间在 0.5-2 ms 之间比较平衡。另外,异步通信对显存管理要求更高,要小心 buffer 复用导致的竞态。

4.5 通信量压缩的落地

通信压缩相对好实现。最简单的做法是在发送前把 FP16 转成 FP8,接收后再转回来:

def compress_for_comm(tensor, dtype=torch.float8_e4m3fn): """发送前压缩""" scale = tensor.abs().max() / 448.0 # FP8 e4m3 的最大值约 448 compressed = (tensor / scale).to(dtype) return compressed, scale def decompress_after_comm(compressed, scale, target_dtype=torch.float16): """接收后解压""" return (compressed.to(target_dtype) * scale)

这里用了一个 per-tensor 的 scale,简单但精度一般。如果追求更高精度,可以用 per-channel 或 per-group 的 scale,代价是额外的元数据传输。

注意:FP8 的两种格式 e4m3 和 e5m2 精度特性不同。e4m3 尾数位多,适合精度要求高的场景;e5m2 动态范围大,适合数值波动大的场景。MoE 的隐藏状态一般用 e4m3 就够。

5. 常见问题与排查技巧实录

5.1 通信瓶颈的定位方法

优化之前先定位。很多人一上来就改代码,结果改了半天发现瓶颈根本不在通信。

第一步,用 profiler 看时间分布。PyTorch Profiler 或者 nsys 都能看到每个 kernel 的耗时。如果 all-to-all 相关的 kernel 占比超过 30%,通信就是主要瓶颈。

# 用 nsys 抓取一段推理的 timeline nsys profile -o moe_profile python infer.py

第二步,单独测通信。把计算部分注释掉,只跑通信,看纯通信耗时。如果纯通信耗时接近端到端耗时,说明通信和计算没有重叠。

第三步,看 PCIe 利用率。nvidia-smi dmon或者dcgm能看到 PCIe 的收发速率。如果传输时速率远低于链路峰值,说明是小包效率问题或者调度问题。

5.2 典型问题速查表

现象可能原因排查方向解决思路
多卡比单卡还慢通信开销超过并行收益看 profiler 通信占比减少通信量,或改用更少卡
PCIe 带宽利用率低小包传输、调度不当看传输包大小分布增大 chunk,合并小包
某张卡明显拖后腿专家放置不均衡看各卡通信量重新做专家放置
通信和计算不重叠同步点太多看 timeline 是否有空窗引入异步通信和流水线
精度明显下降通信压缩过度对比压缩前后输出提高通信精度或加误差补偿
偶发卡死/超时NCCL 配置或拓扑问题看 NCCL 日志调整 NCCL 参数或拓扑

5.3 独家避坑技巧

技巧一:先跑通再优化。别一上来就上压缩和流水线。先用最朴素的实现跑通,确认功能正确,再逐步加优化。每加一个优化都测一次,确认收益为正。我见过太多人一次性堆一堆优化,结果出问题不知道是哪个引起的。

技巧二:关注 PCIe 的“降速”问题。消费级主板多卡时,PCIe 链路可能因为通道分配而降速(比如从 x16 降到 x8)。用nvidia-smi -q确认实际链路宽度和速率,别假设它跑在满速。如果发现降速,检查主板 BIOS 的 PCIe 拆分设置。

技巧三:NCCL 参数要调。默认的 NCCL 配置不一定适合消费级 PCIe 拓扑。可以试试设置NCCL_P2P_DISABLE=1强制走 PCIe(有些平台 P2P 反而更慢),或者调整NCCL_BUFFSIZE和NCCL_NTHREADS。这些参数对通信性能影响很大,值得花时间调。

技巧四:显存和通信要一起考虑。通信压缩省了带宽,但压缩和解压本身要占显存和算力。如果显存本来就紧张,压缩带来的额外 buffer 可能让显存爆掉。做优化时要算总账,不能只看通信。

技巧五:batch size 不是越大越好。大 batch 摊薄了通信启动开销,但也增加了单次通信的数据量,可能让 PCIe 传输时间超过计算时间,重叠失效。找到通信时间和计算时间的平衡点,才是最优 batch size。

6. 这套思路还能怎么扩展

ThunderEP 针对的是消费级 GPU 上的 MoE 专家并行,但它的核心思想——针对低带宽互联重新设计通信模式——可以迁移到很多场景。

比如多机推理场景,机器间走的是以太网或 InfiniBand,带宽特性又和 PCIe 不同,但“通信量压缩 + 通信计算重叠 + 负载均衡”这三板斧依然适用。再比如边缘设备上的模型并行,互联带宽更低,对通信的敏感度更高,ThunderEP 的思路可能更有价值。

我个人在实际操作中的体会是,消费级平台做分布式推理,通信优化的重要性往往被低估。大家习惯性地关注算力、显存、模型精度,却忽略了卡间那条通道才是真正的隐形瓶颈。把通信这关过了,多卡的性价比才能真正体现出来。

最后分享一个小技巧:如果你暂时不想动框架代码,可以先从专家放置这个最容易改的点入手。很多时候,仅仅是把热门专家重新分配一下,就能带来 10%-20% 的端到端提升。这个投入产出比,比啃通信调度代码高多了。

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

从屏幕到硬件:AI Agent控制真实设备的关键技术与SDK开源实践

最近 AI 圈让我最兴奋的消息,不是又一个参数规模更大的模型发布,而是 Muse 这款 AI Agent 应用登顶 App Store,并且把它的 SDK 开源了。作为长期关注 AI Agent 从云端走向终端、从对话走向行动的人,这件事比表面看起来要严重得多…

作者头像 李华
网站建设 2026/10/26 16:38:57

VC++手搓CA系统:数字证书签发与签名验证实战拆解

简介:这份资源是面向高校计算机与信息安全专业学生的毕业设计参考包,围绕CA系统(证书授权中心)展开,帮助读者理解数字证书与数字签名的原理并完成可运行的小型CA实现。内容涉及公钥基础设施、RSA公钥加密、SHA-1或MD5哈…

作者头像 李华
网站建设 2026/10/26 16:38:56

基于遗传算法的LQR控制器优化设计:MATLAB实现与调参避坑指南

简介:这份资源面向控制工程、自动化及车辆工程方向的学习者与研究者,聚焦LQR控制器设计中加权矩阵难以确定这一痛点,提供一套基于遗传算法进行全局寻优的完整案例。资源以汽车主动悬架为被控对象,将性能指标作为目标函数&#xff…

作者头像 李华
网站建设 2026/10/26 16:38:18

机器人模仿学习实战:ACT与Diffusion Policy算法解析

1. 项目概述LeRobot是一个专注于机器人学习与控制的开放框架,第二章教程将深入探讨模仿学习算法及其训练Pipeline的实现。模仿学习作为机器人技能获取的重要途径,通过观察专家演示来学习策略,避免了传统强化学习中的探索成本问题。在实际机器…

作者头像 李华
网站建设 2026/10/26 16:36:33

Muse和Dots:轻关系社交如何破解社交疲劳

说句实话,我第一次在应用商店看到“Muse”和“Dots”这两个名字的时候,第一反应是:“又是哪个海外小众社交产品被搬运到国内了?”但仔细用了几周,又跟踪了一圈国内团队的动作之后,我得说一句:这…

作者头像 李华
网站建设 2026/10/26 16:34:00

B+Tree索引原理与数据库性能优化实践

1. 为什么BTree成为数据库索引的标准选择 第一次接触数据库索引时,很多开发者都会好奇:为什么大多数关系型数据库都选择BTree作为默认的索引结构?这个问题看似简单,实则涉及计算机科学中数据结构设计与硬件特性的深度权衡。 在My…

作者头像 李华