《Nano-vLLM 源码解读》第 19 篇 · MLP 与 SiluAndMul:合并的 SwiGLU

nano-vllm 用千行代码拆解 vLLM 核心,是读懂大模型推理最快的捷径。

1. 介绍

上一篇拆解了残差流的首尾两站(embed_tokenslm_head)。回到中间的 decoder 层:L13 展示的骨架是 input_norm → self_attn → post_norm → mlp,L14 拆解了两处 RMSNorm、L17 拆解了 self_attn——层里只剩最后一块 MLP。

Qwen3 的 MLP 做三件事:升维 → 门控 → 降维gate_up_proj 一次 matmul 把 hidden 升到两份宽中间层 gate 和 up,SiluAndMul 把它拆回两半做 silu(gate) * up 的门控,down_proj 再投回 hidden。

本篇的核心概念是 SwiGLU:普通 MLP 中间层每个通道都固定地过一次激活,SwiGLU 多引一条投影 gate 当「软闸门」,每个通道放多少根据输入自适应。

2. 总览

MLP 站在 decoder 层的后半段:post_attention_layernorm 把残差流末端归一化后输入 MLP,MLP 把它升维、门控、再降维,输出一个加回残差流的「增量」。

在这里插入图片描述

配置项含义
hidden_size1024残差流宽度 / MLP 的进出维度
intermediate_size3072MLP 中间层宽度(gate、up 各一份)
hidden_act“silu”激活函数,Qwen3 固定 silu(SwiGLU 的 S)

图中三段对应后续三节:3.1 升维(gate_up_proj)、3.2 门控(SiluAndMul / SwiGLU)、3.3 降维(down_proj)。

3. 打开 Qwen3MLP:升维 → 门控 → 降维

Qwen3MLPforward 只有三行,正好对应三个子模块:

gate_up = self.gate_up_proj(x)   # 升维:1024 → 6144
x = self.act_fn(gate_up)         # 门控:6144 → 3072(SiluAndMul)
x = self.down_proj(x)            # 降维:3072 → 1024

一个 token 的 hidden 是 1024 个数,相当于它「当前理解」的一份摘要。三步各做一件事:

  • 升维gate_up_proj,1024→6144):把紧凑的 hidden 摊开到更宽的中间层,一次算出 gate、up 两栏,各 3072(合计 6144)。为什么要先摊宽:在原来 1024 个数里直接算,能拼出的花样有限;摊到三倍宽,给后面门控更多通道去施展,模型能表达的变化更丰富,算完再压回 1024。宽度由 intermediate_size 定。
  • 门控SiluAndMul,6144→3072):用 gate 当软闸门逐元素缩放 up——重要的留下、次要的压小,得 3072。为什么要门控:不加 gate 的话,每个通道都一视同仁地过激活、放多少跟当前输入无关;加上这道闸门,同一个通道面对不同输入能开大或关小,模型才会按输入挑重点。闸门怎么开(SwiGLU)是本篇核心,见 3.2。
  • 降维down_proj,3072→1024):门控挑出的是 3072 个挑过重点的数,可开头那份摘要只有 1024 个数,长短不一没法相加。down_proj 把这 3072 个数重新组合、浓缩回 1024 个,凑成和摘要一样长——这 1024 个数不是一份新摘要,而是「该在原摘要上改多少」的修改量,逐位加到摘要上,这个 token 的理解就更新一点。

整体是「窄→宽→窄」:中间放宽(intermediate 3072)给足容量,进出两端都对齐残差流的 1024 宽度。

# Qwen3MLP(qwen3.py 真实源码 + 注释):升维 → 门控 → 降维
from torch import nn
from nanovllm.layers.activation import SiluAndMul
from nanovllm.layers.linear import MergedColumnParallelLinear, RowParallelLinear

class Qwen3MLP(nn.Module):
    def __init__(self, hidden_size, intermediate_size, hidden_act):
        super().__init__()
        # 升维:gate、up 两个投影合并成一次 matmul(合并投影,省一次 kernel launch)
        #   输出 = [intermediate]*2 = gate(3072) ⊕ up(3072) = 6144
        self.gate_up_proj = MergedColumnParallelLinear(
            hidden_size, [intermediate_size] * 2, bias=False)
        # 降维:把门控后的中间层投回 hidden
        self.down_proj = RowParallelLinear(
            intermediate_size, hidden_size, bias=False)
        assert hidden_act == "silu"      # Qwen3 固定 silu(SwiGLU 的 S)
        self.act_fn = SiluAndMul()       # 门控:silu(gate) * up

    def forward(self, x):
        gate_up = self.gate_up_proj(x)   # [N,1024] → [N,6144]
        x = self.act_fn(gate_up)         # [N,6144] → [N,3072]
        x = self.down_proj(x)            # [N,3072] → [N,1024]
        return x

3.1 gate_up_proj

升维这一步把 hidden(1024)投到更宽的中间层。Qwen3 的 MLP 需要 gate、up 两份中间层(各 intermediate_size=3072),但没有建两个投影,而是用一个 MergedColumnParallelLinear(1024, [3072, 3072])——把两个本该各自 [3072,1024] 的权重拼成一个 [6144,1024],一次 matmul 算出,输出 6144 = gate(3072) ⊕ up(3072) 头尾相接。

gate、up 用的是同一个输入 x,合成一次大 matmul 比分两次少一次 kernel launch。

3.2 SiluAndMul:拆回两半做门控

升维得到的 gate_up(6144)马上被 SiluAndMul 切回两半做门控。这是本篇的核心——SwiGLU。

SiluAndMulsilu(gate) * up。先把 6144 的宽向量从中间 chunk(2,-1) 切成 gate、up 两半(各 3072),对 gate 过一个 silu,再和 up 逐元素相乘,得 3072 维。silu(x) = x·σ(x)(σ 是 sigmoid):σ(gate) 落在 0 到 1 之间,像每个通道的「开度」;silu 在开度上又乘了 gate 自身的大小。这套「gate 当门、控 up 通过多少」加 silu 激活的组合,就叫 SwiGLU(Swish-Gated Linear Unit,Swish 即 silu)。

打个比方:up 是一股股要往下流的水,gate 经 silu 后是一排阀门——每个阀门按自己的开度,决定对应那股水放多少过去。

σ(x) 本身能表示开度,为什么还要乘 x:开度 σ 本就落在 0~1,作系数只能把 up 关小、最多原样放行;而且会饱和——gate=3 和 30 的 σ 都≈1,顶到满开、分不出轻重。silu 在开度上再乘 gate 自己,整段乘子 silu(gate) 就突破了 0~1:gate 大时能超过 1(放大)、gate 略小于 0 时还能微微为负,gate 的强弱也被留了下来。代价是这个「阀门」能开过头、不再是干净的 0~1;但实测就是 silu 当门(SwiGLU)比纯 σ 当门(经典 GLU)更好用。

解决了什么:让同一套 MLP 权重按 token 自适应。比如一个 MLP 同时要处理代码里的 { 和散文里的「快乐」——{ 进来时 gate 开大一批通道、关小另一批,「快乐」进来时换一组开关,等于在同一套权重里被门控临时「裁」出两个不同的子网络。普通 MLP 没这道闸门,所有 token 都按同一条激活曲线过,token 间的差异只能靠权重。简而言之,既起到闸门的选择作用,又保留了 gate 的强弱。实测的收益很直接:在差不多的模型规模下,把普通 FFN 换成 SwiGLU,预训练 loss 更低、下游任务也更好。

在这里插入图片描述

# SiluAndMul(activation.py 真实源码 + 注释):SwiGLU 的门控
import torch
import torch.nn.functional as F

class SiluAndMul(nn.Module):
    # @torch.compile 把下面两步融成一个 kernel
    @torch.compile
    def forward(self, x):
        x, y = x.chunk(2, -1)            # 6144 切回两半:x=gate(3072)、y=up(3072)
        return F.silu(x) * y             # silu(gate) 当开度,逐元素乘 up

import torch
import torch.nn.functional as F

# 造一条宽中间层(N=2、宽=8 便于打印),手算 SwiGLU
gate_up = torch.randn(2, 8)
gate, up = gate_up.chunk(2, -1)          # 切成两半:gate、up 各 [2,4]
out = F.silu(gate) * up                  # SwiGLU:silu(gate) * up

print("宽中间层减半 :", tuple(gate_up.shape), "->", tuple(out.shape))  # (2,8)->(2,4)

# 验证 silu 定义:silu(x) = x * sigmoid(x)
silu = gate * torch.sigmoid(gate)
print("silu = x·σ(x) :", torch.allclose(out, silu * up))             # True
# 开度 σ(gate) 落在 0~1:门开多少由 gate 自己定
print("开度 σ(gate)  :", torch.sigmoid(gate)[0].tolist())

宽中间层减半 : (2, 8) -> (2, 4)
silu = x·σ(x) : True
开度 σ(gate)  : [0.9031739234924316, 0.7533642053604126, 0.2760199010372162, 0.6877664923667908]

3.3 down_proj:投回 hidden

门控后的中间层是 3072 维,down_projRowParallelLinear(3072, 1024))把它投回 hidden 的 1024 维,结束 MLP。单卡下就是一次普通 F.linear

要注意 MLP 的输出是残差流的「增量」,不是新的残差流本身。把它加回残差流的那个 +,由下一层的 input_layernormadd_rms)完成。

回看这一节:gate_up_proj 合并 gate、up 为一次 matmul(合一为快),SiluAndMul 拆回两半做门控(拆开为门控),down_proj 投回 hidden。升维、门控、降维三步走完,一层 decoder 的模块就全齐了。

4. 集成验证

加载真实 Qwen3-0.6B,取第 0 层的 mlp,喂一条 [N,1024] 的 hidden,手动走升维 → 门控 → 降维三步,看 shape 流 1024 → 6144 → 3072 → 1024,并验证手动三步与 mlp(x) 完全一致。

import torch
import torch.distributed as dist
from modelscope import snapshot_download
from nanovllm.config import Config
from nanovllm.engine.model_runner import ModelRunner

torch.cuda.set_device(0)

model_path = snapshot_download("Qwen/Qwen3-0.6B")
config = Config(model_path, enforce_eager=True, max_model_len=4096)
runner = ModelRunner(config, 0, [])
mlp = runner.model.model.layers[0].mlp    # 第 0 层的 Qwen3MLP

print("gate_up_proj.weight :", tuple(mlp.gate_up_proj.weight.shape))  # (6144, 1024) 升维
print("down_proj.weight    :", tuple(mlp.down_proj.weight.shape))     # (1024, 3072) 降维

/opt/conda/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm


Downloading Model from https://www.modelscope.cn to directory: /root/.cache/modelscope/hub/models/Qwen/Qwen3-0.6B


2026-06-17 16:25:13,706 - modelscope - INFO - Target directory already exists, skipping creation.


gate_up_proj.weight : (6144, 1024)
down_proj.weight    : (1024, 3072)
# 喂一条 [N,1024] 的假 hidden, 查看 shape
x = torch.randn(4, 1024, device="cuda", dtype=mlp.gate_up_proj.weight.dtype)

with torch.inference_mode():
    gate_up = mlp.gate_up_proj(x)         # 升维
    mid = mlp.act_fn(gate_up)             # 门控(SiluAndMul)
    out = mlp.down_proj(mid)              # 降维
    full = mlp(x)                         # 一次 forward 对照

print("升维 :", tuple(x.shape), "->", tuple(gate_up.shape))    # (4,1024)->(4,6144)
print("门控 :", tuple(gate_up.shape), "->", tuple(mid.shape))  # (4,6144)->(4,3072)
print("降维 :", tuple(mid.shape), "->", tuple(out.shape))      # (4,3072)->(4,1024)
print("手动三步 == forward :", torch.allclose(full, out))       # True

升维 : (4, 1024) -> (4, 6144)
门控 : (4, 6144) -> (4, 3072)
降维 : (4, 3072) -> (4, 1024)
手动三步 == forward : True

5. 小结

Qwen3 的 MLP 分三步走:gate_up_proj 升维到两份宽的中间层(6144 = gate ⊕ up),SiluAndMul 拆回两半做 silu(gate) * up ,维度降到 3072,down_proj 投影回 hidden 1024。门控就是 SwiGLU——gate 当软闸门,让每个通道放多少依输入自适应,比普通 MLP 固定过激活更灵活。

一条主线贯穿:gate_up_proj 把 gate、up 合成一次 matmul(合一为快),SiluAndMul 紧接着 chunk 拆回两半(拆开为门控)。合并与拆分的对称,是这一层值得记住的设计。

拆解完 MLP,一层 decoder 的模块就齐了。下一篇讲解权重加载:packed_modules_mapping 怎么把 HF 分开的 gate_projup_proj 装进合并的 gate_up_proj

于2024年4月-2025年9月期间,研究团队在贵州习水国家级自然保护区制定39条样线,涵盖灌木林、常绿阔叶林、针叶林、常绿落叶阔叶混交林、针阔混交林等不同植被类型,每条样线分春夏秋冬4个季节采集样品,用真菌采集软件记录经纬度、海拔、采集地点、时间、生境等信息,使用佳能相机(R6 mark Ⅱ)对大型真菌进行拍照,并采集标本,标本存放于贵州省生物研究所大型真菌标本馆(HGAMF)。 通过形态学初步鉴定,结合分子生物学最终鉴定,参考已]报道的中国毒蘑菇名录开展毒蘑菇的认定。 调查到保护区内有毒真菌7目25科64种,导致中毒的主要类型有急性肾衰竭型、神经精神型和胃肠炎型。最终形成贵州习水国家级自然保护区大型有毒真菌图片数据集,它由以下2个部分组成。 (1)附件1包含78张原始照片(.JPG),照片名字包括了大型有毒真菌的拉丁名和中文名,若无中文名的直接用拉丁名。 (2)附件2是一个压缩文件,包含了2张工作表,其中一张表是大型有毒真菌39条样线的信息,另一张表是大型有毒真菌的中毒类型。 照片采用佳能相机R6 mark Ⅱ拍摄,物种鉴定通过多种文献核实,并经两位以上专家鉴定确认。该数据集可为研究地及周边的普通人识别有毒大型真菌提供参考,通过及时的图片对比,能有效避免误采误食大型有毒真菌,同时为因误食大型真菌可能引发的身体损伤进行了总结,能为患者及时治疗提供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值