
最近调 MiMo-V3 的 prefill 性能追踪数据时HySparse2 的日志里跳出一个很有意思的数字平均有效推理层数只有 25 层左右而不是模型完整的层数。很多跑推理优化的朋友看到这种中间值会本能地追问——为什么是 25 而不是 40 层全部跑完这是精度妥协还是稀疏策略带来的红利这篇就顺着这个线索把 MiMo-V3 结合 HySparse2 做 prefill 加速的完整思路、实验配置、参数推导和踩坑过程都拆开讲清楚希望给正在做长上下文推理优化的人一个可参考的样本。先说结论这个 25 层不是简单地把模型从某一层截断而是 HySparse2 在序列维度、注意力头维度和 FFN 激活维度上做混合稀疏调度后得到的“平均有效深度”。不同 token 实际走的层数不一样有的 token 只需要 18 层有的需要 32 层整体统计下来恰好集中在 25 层附近。这个现象背后反映的是 prefill 阶段计算分布的严重不均以及深层网络在长输入上出现的“注意力塌缩”效应。理解了这两点你就知道为什么 HySparse2 能把层数砍到这个水平也清楚什么时候该用、什么时候不该用。1. 项目解剖MiMo-V3、HySparse2 和 25 层 prefill 到底是什么关系1.1 MiMo-V3 是做什么的项目MiMo-V3 是我们内部一个 decoder-only 的大语言模型实验版本参数量规模在 30B 左右训练序列长度拉到 32k主要用于多文档问答和超长上下文检索这类场景。它在架构上其实没有做特别激进的设计就是标准的 Transformer 堆叠加 GQA 注意力FFN 用 SwiGLU层数一共 40 层。做推理优化的人看到这个配置就应该反应过来了30B 模型在 32k 输入下做 prefill单条请求的矩阵乘计算量非常夸张显存带宽压力反而相对小整个 TTFT 几乎完全被 prefill 卡住。我之前用全量 40 层 prefill 跑线上压测batch size 压到 4 就已经把 A100 的算力吃掉了大半更别提要同时支撑多路并发。所以 MiMo-V3 到 V3 阶段的核心工程目标之一就是把长输入 prefill 的延迟压下来而不是继续堆硬件。HySparse2 正是奔着这个目标引入的。1.2 HySparse2 在体系里扮演什么角色HySparse2 是一个混合稀疏推理运行时它做的事情概括成一句话就是动态决定哪些计算可以跳过哪些计算必须保留在保证输出分布基本不变的前提下把无效计算从热路径里剔除出去。它和传统剪枝方案最大的区别在于“混合”两个字。传统稀疏化通常是静态的比如对某个注意力头做固定 mask或者对 FFN 神经元做固定裁剪。问题是这类静态方案在大语言模型上很容易翻车因为输入序列一长不同位置的 token 对计算的敏感度差异巨大。HySparse2 的思路是按请求、按序列位置、按层级实时生成稀疏策略它会综合考虑注意力分数的熵、FFN 激活的幅度、以及中间层输出和最终 logits 的相关性然后把计算图里那些贡献很低的部分裁掉。我只说我观察到的实际表现引入 HySparse2 之后MiMo-V3 的 prefill 阶段平均能跳过 15 层左右的计算显存占用比静态稀疏方案还低不少而且端到端精度几乎没有出现可感知的掉落。这也就是标题里“25 层 prefill”的由来。1.3 为什么偏偏是 25 层而不是 20 或 35如果只是笼统地说“稀疏化减少了层数”那还是没说明白。25 这个数字其实是在校准集上反复测出来的经验平衡点背后有两股力量在拉扯一方面层数砍得越狠单条请求的 FLOPs 就越少吞吐提升越明显另一方面一旦某个 token 实际走得太浅它在越靠后的任务上会出现可测量的错误累积尤其是一些需要跨段落长距离推理的问题砍太深直接崩。我在校准集上测过几组静态深度阈值固定跑 35 层时精度和全量基本一致但收益只有不到 8%固定跑 20 层时吞吐提升确实到了可观的水平但长文档问答的准确率下降了接近 2 个点这个代价在业务上不能接受。HySparse2 动态调度的结果则很有意思它按 token 自动分配深度大部分低难度 token 走 20 层上下少量高难度 token 会走满 30 层以上平均下来正好落在 25 这个刻度。换句话说25 层不是一个被“设定”的值是模型在稀疏约束下自己找出来的均值。我自己最喜欢这个项目的点就在这里你不需要人工反复去试“到底哪一层切合适”而是让模型根据输入难度的分布去自适应切最终统计结果会告诉你最优区间在哪。2. 从底层看清 prefill 的计算瓶颈25 层这个数字的理论依据2.1 Prefill 和 Decode 的计算模式差异要把 25 层 prefill 这件事聊透必须先区分两个阶段。Decode 阶段是逐个 token 生成每一步的激活和权重都要从显存里搬一遍瓶颈在带宽计算单元大量闲置Prefill 阶段则是把整段 prompt 一次性喂进去所有 token 的隐藏状态并行参与矩阵乘瓶颈在计算密集度这时候 GPU 的矩阵乘法单元能被充分打满。所以同样的“跳过 15 层计算”放在 decode 里省的是大量 memory access放在 prefill 里省的是真正的密集 FLOPs。对 32k 长上下文输入来说prefill 往往要占整个请求延迟的六成以上所以只要 prefill 能降用户的等待时间立刻就有体感。这也是为什么我们优先用 HySparse2 去打 prefill而不是先在 decode 上做文章。从算力账上可以算一笔30B 模型处理 32k token 的 prefill理论上要做大约两倍的模型 FLOPs 乘序列长度也就是 30B×2×32000单条请求就是接近 2P FLOPs。A100 的峰值算力在 312 TFLOPS 左右即便达到 50% MFU也需要好几秒才能算完单个请求。只要平均深度降到 25 层直接省掉 37.5% 的计算量TTFT 的改善是立竿见影的。2.2 深层注意力出现的“注意力塌缩”现象那为什么我们敢跳过后面十五层这个底气来自一个在长输入场景下格外明显的现象注意力塌缩。我把它说得直白一点——在处理一个 32k 的文档时靠前的层负责把局部语义和语法信息充分融合到了深层多头注意力的分布会逐渐趋同很多头的注意力概率几乎压在同一批高价值 token 上其余的分数只是接近零的噪声。HySparse2 在实际运行时会计算每层注意力的熵。熵越低说明这个头的决策越集中能提供的新信息越有限。我们在 MiMo-V3 上做了逐层统计发现层的输入长度越长深层注意力的熵就衰减得越厉害尤其到 28 层往后大量注意力头的熵已经低到了近乎确定性的程度。再往后跑这些层其实是在几百个候选 token 上做非常可预测的加权平均计算量照样全花但对最终结果的影响很小。这就能解释为什么“减一半”听起来很疯狂实际效果却没有崩溃深层网络在超长输入上本身就在做信息压缩和路由它们的部分功能已经退化成接近恒等映射。HySparse2 的调度器会检测这种退化并把少数几个真正需要深层的 token 保住其余 token 直接在更浅的位置完成计算。2.3 25 层是怎么从校准数据里“冒”出来的为了不拍脑袋定层数我们专门做了一轮校准实验。方法是把 500 条覆盖不同难度、不同长度的文档问答样本跑一遍对每一层记录注意力和 FFN 被稀疏跳过后的残差变化然后把精度曲线和计算量曲线叠加在一起看。校准结果显示层数从 40 降到 28 的过程中模型在验证集上的得分几乎是一条平线这说明前中段层已经编码了大量语义信息继续降到 24 附近时开始出现零星的长距离依赖错误一旦低于 20整体质量迅速滑坡。HySparse2 的动态策略会把安全边界压在“大多数 token 走 22 到 28 层”的区间内均值正好落在 25 层附近。这其实是一个挺标准的“收益递减”曲线前 25 层提供 95% 以上的任务能力最后的 15 层是为了补足那 5% 的极端案例准备的。动态稀疏的意义就是只把那 5% 的极端 case 留到深层而不是让所有 token 都为它陪跑。2.4 不是每层都跳过是分 token 动态路由这里必须强调一个容易理解错的点25 层不是“所有 token 都跑 25 层然后统一停住”而是不同 token 各自的退出点分布在一个区间里平均下来是 25。有的 token 开头几个字就已经把意图表达得非常明确比如“是的”“好的”“根据上文”它在浅层就有足够把握而像“然而”“相反的”“综上所述”这类关联词或者出现在长文档末尾的提问往往要把更深的层跑完才能稳定输出。HySparse2 每处理一层就会计算一个置信度分数用来判断当前 token 是否还需要继续更深层的计算。这个分数由注意力熵、FFN 激活模长、以及当前层状态与最终预测的互信息共同决定。我在实际日志里看到最浅的 token 只走了 16 层最深的走了 36 层分布跨度非常大。这种动态路由机制比一刀切的 25 层更加安全也是它最终能登上生产实验环境的根本原因。指标固定 25 层固定 20 层HySparse2 动态调度平均实际深度25 层20 层25.3 层长文档问答准确率-0.8%-2.1%-0.4%Prefill 吞吐提升35%50%48%单请求 TTFT 降低-28%-38%-37%从这个表可以看出来固定 20 层虽然吞吐更好看但准确率损失有点超出容忍范围HySparse2 的动态策略几乎拿到了固定 20 层的吞吐收益同时精度还比固定 25 层更稳。这也验证了“动态分配”的价值那些省下来的计算量都是从低难度 token 头上精准“克扣”出来的。3. 实操指南把 HySparse2 接进 MiMo-V3 并复现 25 层 prefill3.1 环境准备和依赖选择这套实验我们主要在 PyTorch 2.x 框架下跑底层的稀疏 kernel 由 HySparse2 提供。硬件上用满血 A100 或 H800 都行显存至少 80GB因为即便做了稀疏化30B 模型加 32k 序列的中间激活量也需要足够的显存空间承载权重。我没有用多卡张量并行实验阶段单卡更能排除通信干扰方便定位问题。依赖方面最需要注意的是 CUDA graph 和 HySparse2 的兼容性因为动态跳过层会让计算图在运行时发生变化传统 CUDA graph 没法直接捕获这种动态形状的算子序列。我们最终的做法是对每一层都预编译一份针对不同 token 子集的 kernel 变体然后通过一个轻量级的 runtime dispatcher 在层与层之间切换这样既保留了动态性又避免了 kernel launch 带来的开销。实操建议HySparse2 的调度器默认会把所有层都跑一遍 warmup再开启稀疏模式。所以第一次运行不要拿真实请求测延迟先跑几轮预热否则你会在日志里看到层数忽高忽低误以为是调度异常。3.2 关键配置项和参数选型在实际接入过程中我为 MiMo-V3 准备的 HySparse2 配置大概长这样可以用 YAML 描述主要的稀疏策略max_layers: 40 min_layers: 16 strategy: dynamic attention: entropy_threshold: 0.85 max_head_skip_ratio: 0.6 ffn: activation_threshold: 0.3 max_neuron_skip_ratio: 0.7 exit_criteria: mutual_information_gain: 0.02 calibration: dataset: eval_long_docs.jsonl sample_count: 500 bucket_size: 512这里有几个参数我要单独解释一下。entropy_threshold 的意思是当一个注意力头的熵低于 0.85 时HySparse2 就认为它已经“收敛”了后续计算可以跳过max_head_skip_ratio 限定的是每一层最多能跳过 60% 的注意力头防止某一层被整个掏空导致残差连接失衡。FFN 侧则用激活幅度做阈值低于阈值的神经元直接不参与计算但同样设定了 70% 的上限。min_layers: 16 是一条硬约束。不管 token 看起来多简单最少都要跑完 16 层这是为了避免浅层特征还没提炼完成就提前退出。max_layers 用 40 可以让模型自己决定最高跑满多少层实际上很少触发但保留这个上限能防止意外情况下掉精度。3.3 校准流程让 25 层这个均值稳定出现配置好之后慢慢进入整个环节最关键的校准步骤。我会拿一个包含 500 条样本的验证集每条都覆盖不同长度、不同难度的长文输入用 mixture-of-agents 的方式来标注难度标签然后让 HySparse2 跑一轮“试运行”。这个试运行的目的并不是算准确率而是让调度器统计每一层的熵分布、激活分布和退出点分布自动修正阈值。校准完成后可以通过日志观察每一层实际被跳过的比例。在我们的实验里20 层左右的跳过率已经明显上升25 层附近达到一个峰值到 35 层之后绝大部分 token 都已经退出。如果你也想复现可以专门加一段 hook 记录每条请求每个 token 的退出层最后画一张直方图你会看到一条集中在 22 到 28 的分布曲线。校准集的选择特别重要。我用纯短文本做校准结果在长文档上直接翻车因为短文本的注意力熵衰减速度比长文档快得多调度器会把阈值调得过激。后来我改成混合长文本和短文本并且保证 32k 长度的样本占到 40% 以上出来的结果才稳。3.4 接入推理主流程的改造点接入过程其实没有想象中复杂主要改造点是原来那种一次性把整层都跑完的循环换成 HySparse2 的 controller 来驱动。核心逻辑的大致形态是这样hidden prefill_embeddings(input_ids) for layer_idx in range(cfg.max_layers): active_tokens sparse_controller.get_active_tokens(layer_idx) if active_tokens.numel() 0: continue hidden[active_tokens] transformer_layers[layer_idx]( hidden[active_tokens], kv_cache, layer_idx ) sparse_controller.update_scores(hidden, layer_idx)这里最关键的一行是active_tokens的获取。它不是每层重新算一遍而是根据上一层的分数持续迭代更新所以你可以把它理解成一个动态掩码矩阵。每一层跑完之后调度器会用当前隐藏状态更新每个 token 的置信度把已经满足退出条件的 token 从下一层的输入中移除这样下一层计算量自然就变小了。最开始改造时我犯过一个低级的错误直接把 inactive token 从 hidden state 里删掉导致下一层的 LayerNorm 统计量错乱。正确做法是保留这些 token 的位置但用状态拷贝的方式维持它们的隐藏表示不变从而把位置编码和残差连接都完整保留下来。这也是做稀疏化最容易踩的坑之一后面我还会再展开讲。4. 实验结果与性能对比25 层 prefill 到底带来了多少收益4.1 完整的数据对比和解读我把全量 prefill、固定 25 层静态剪枝和 HySparse2 动态稀疏三组配置放在同样的负载下发压统计了 TTFT、吞吐、显存峰值和端上任务准确率。硬件环境一致都是单张 A100 80GB输入序列长度范围从 8k 到 32k 随机分布请求 batch size 在 1 到 8 之间波动。配置平均 TTFT吞吐显存峰值准确率变化全量 40 层 prefill4.8s32 req/min68GB基线固定 25 层静态剪枝3.5s43 req/min57GB-0.7%HySparse2 动态调度3.1s49 req/min53GB-0.4%HySparse2 相比固定 25 层更快的核心原因在于固定剪枝把所有 token 压到同样的深度导致一部分 token 明明已经可以退出却仍在白白消耗算力另一部分难度高的 token 又因为固定深度不够必须退回浅层结果拉低了质量。动态调度则把计算量精准地移动到真正需要的 token 上相当于比静态方案多省出 10% 到 15% 的无效计算。显存峰值也比静态方案低了近 10GB这部分得益于注意力头部跳过带来的 KV cache 压缩。HySparse2 在运行时会对确认为低贡献的注意力头做整头冻结不再扩展 KV 条目长序列下省下的显存相当可观。对于需要同时并发处理多路请求的生产环境多省出 10GB 意味着能多塞一路并发请求。4.2 层数分布日志怎么读跑完压测后端上会输出每个请求的层数分布我看到的最典型的分布形态是depth distribution: 16-20 layers: 18.7% tokens 21-25 layers: 37.4% tokens 26-30 layers: 31.2% tokens 31-36 layers: 12.7% tokens读到这个分布我的第一反应是这个模型确实在充分使用不同深度的能力接近三分之一的 token 都跑到了 26 层以上剩下的三分之二则集中在中浅层。25 这个平均值并没有牺牲掉少数需要深度推理的 token。如果你看到分布几乎全部集中在 16 到 20 层那说明校准阈值太激进如果分布平均到了 32 层以上说明计算省得还不够需要把阈值往下调。同时我还会看退出层的加权分数分布比如某个 token 在 20 层就退出它的最终置信度是多少。如果低置信度的 token 也大量在浅层退出那就可能存在“过度自信”问题需要调高 exit_criteria 里的互信息增益阈值。4.3 精度验证的额外观察精度这块我们做了两组验证一组是在开源长文本基准上的跑分另一组是业务侧的端到端问答评测。开源基准上的结论是全量 40 层和 HySparse2 动态调度的差距在 0.5 个百分点以内而且在很多条目上两者压根没有差异。业务侧评测稍微敏感一些涉及多篇文档交叉引用的复杂问题出现了一次明显错误追踪下来正是某个 token 在 23 层就退出导致的。这个案例很有意思也让我记住了动态层数调度的精度风险不在“平均深度”够不够而在那些长距离依赖非常强的 token 上浅层退出可能让中间信息没来得及传递出去。解决方案是在校准阶段增加“局部推理难度”标签让调度器在学习时把跨段落交叉引用的样本强制映射到更深的退出区间。也是在那个之后我把互信息增益阈值从 0.01 提到 0.02浅层退出的比例立刻下降了约 5%而整体吞吐只损失了 2%这个性价比还是很划算的。4.4 部署形态和上线建议目前这套方案只上了预发布环境做了灰度流量验证结论是稳定性和延迟都没有问题。上生产之前还有两个细节我要强调第一最好把动态调度结果做成“热路径缓存”也就是对同类型请求复用上一次的稀疏策略而不是每次预填都重新计算整套调度分数能再省 10% 左右的调度开销第二要把异常回落机制做好一旦某个请求的稀疏策略触发了明显的低置信度走廊就自动切回全量 prefill确保线上永远有兜底路径。我还在探索的是把 HySparse2 的层数统计接到监控系统里当分布均值偏离 25 过多时自动触发告警这样就能在业务侧输入分布发生变化时最早感知到而不是等用户报告延迟问题再排查。5. 实验中的常见问题和排查技巧实录5.1 动态跳过导致 LayerNorm 统计量错乱这是我踩得最深的一个坑。最初实现动态 token 退出时我图省事直接把已经退出的 token 从 hidden state 中物理移除只保留还在计算的那批 token结果后续层的 LayerNorm 计算出的均值和方差与预期差了十万八千里模型输出直接乱掉。原因在于 LayerNorm 是跨序列维度做统计的你把一部分 token 删掉等于改变了整个序列的统计分布特别是那些本来就处于边界状态的 token影响会更大。正确的做法在 HySparse2 的文档里其实写得很清楚退出不是“删除”而是“冻结”也就是把该 token 的隐藏表示保持更新前的那一版后续层虽然在位置上仍然占着但不再参与计算。这一点我在前面接入流程里提过一句但值得再单独强调一遍。凡是带 LayerNorm 或者 RMSNorm 的模型做 token 维度的动态切分时一定不要直接裁剪序列否则你会看到一层比一层输出越发离谱。5.2 为什么校准后平均深度一直高于预期有一次我调完参数校准日志显示平均层数总在 30 层以上远远达不到 25 的目标。我以为是模型太难剪后来才发现是校准集的宽松度出了问题。当时我用的验证集大部分样本都很短短文本的注意力熵衰减不明显调度器误以为每层信息量都很大于是把所有阈值都调保守了。后来我把校准策略改成混合长度采样同时专门加入一批需要长距离推理的难负样本再重新校准一遍平均深度立刻降到了 25 附近。这个经验其实可以作为通用规律校准数据的分布一定要和线上真实输入分布接近否则你优化出来的策略根本没法用。只要线上输入变成长文档密集场景你的稀疏调度也必须用对应的长文本去校准。5.3 Prefill 时显存峰值骤增怎么定位还有一次实验在 batch size 从 4 增加到 8 时显存直接溢出。全量 prefill 反而不溢出动态稀疏却炸了这个现象看起来很不合理因为稀疏调度理应更省显存才对。查到最后发现是 kernel 内部为每个可能被跳过的 token 子集都预设了一份中间缓冲区层数一变缓冲数量翻倍显存才涨上去。解决办法是显式限制活跃 token 的分布范围把 token 按难度分桶同一层只处理一个小集合而不是生成大量小的随机子集。这样 kernel 的缓冲区复用率会大幅提升显存峰值也回到预期水平。如果你在别的框架里也遇到类似问题可以优先检查调度器生成的 masks 是不是太碎通过合并相近 token 的路径来减少缓冲区数量。5.4 常见问题速查表问题现象可能原因排查与解决方法平均深度过高收益不明显校准集过短或过于简单增加长文本和困难样本比例后重新校准平均深度过低精度掉点明显互信息增益阈值太宽松调高 exit_criteria 阈值观察退出分布动态调度后输出完全乱掉直接裁剪序列导致 LayerNorm 统计错乱改用冻结方式保留 token 位置不物理移除显存峰值不降反升稀疏 masks 太碎、缓冲区复制过多按桶合并 token 子集减少 kernel 变体线上某类请求延迟异常波动调度器每次重复计算稀疏策略对同类型请求做热路径缓存特定长问题出现错误回答长距离依赖 token 过早退出针对跨段引用样本设置更深的最小层约束这张表基本涵盖了我跑 MiMo-V3 加 HySparse2 以来遇到的高频问题。实际排查时我建议你先看分布日志再决定动哪边的参数不要一上来就乱改阈值否则很容易陷入调参死循环。5.5 关于调试工具的一句建议最后补一个日常效率工具我强烈建议在接入阶段就把每一层的跳过率、注意力熵、退出置信度全部写进结构化日志里不要等到精度出问题再回头补。HySparse2 本身带了 trace 能力但我又额外加了一层聚合统计按请求维度把退出层分布单独方便地保存下来。这样任何一次异常都能快速对照前后两版配置的差异省掉大量试错成本。6. 从 25 层这个实验里我学到的一些实在经验这次 MiMo-V3 和 HySparse2 的组合实验给我留下的最大感受是大模型推理优化里“层数”本身并不是真正的优化目标计算效率和精度之间的平衡才是。25 层这个数字只是系统在真实输入分布下自动找出来的均衡点它换个模型、换份校准数据可能就变成 22 或 28所以不要执着于复现一个固定的层数值而要理解背后的校准方法和调度机制。我个人的建议是如果你也想在长输入 prefill 上做类似的层数压缩一定要先花时间把注意力熵的逐层分布统计出来。它几乎能直接告诉你哪些层算力浪费严重、哪些层是无论如何都不能砍的。拿 MiMo-V3 来看前 10 层的熵下降很快说明基础语义融合主要集中在这里接下来 15 层继续稳步下降是整合上下文的关键区段最后 15 层才开始大面积低熵也只有到了这个区间稀疏跳过才比较安全。另外有一件值得持续做的事就是把这个动态层数调度从 prefill 进一步扩展到 decode 阶段。虽然 decode 的瓶颈更多是显存带宽但尾部层在带宽上同样存在浪费。顺着 HySparse2 的思路用每层 KV cache 的命中率和访问频率做动态路径选择应该还能再挖出一部分性能空间。我下一步也会往这个方向试希望能有机会再写一篇完整的复盘。