主流大模型全景与选型

不存在「最好的模型」,只存在「在你的约束下最合适的模型」——换一组约束,冠军就换人。

30″30 秒看懂大模型选型

挑模型这件事,本质上和给团队招人一模一样。

你可以请外部顾问——能力最强、来了就能干活、按小时计费,但公司的资料得发给他看;也可以招个正式员工坐进办公室——资料一步不出内网,你还能送他去进修(微调),代价是要给他配工位和显卡。前者是闭源 API,后者是开源权重本地部署

招人时你会看学历、看专业方向、看一次能读完多厚的材料。对应到模型,就是参数量、架构、上下文长度。而最关键的一点两边也一样:简历漂亮不等于干得好,得让他做一套你自己的题

图① 30 秒看懂:请外部顾问还是招正式员工
图① 30 秒看懂:请外部顾问还是招正式员工
招聘里的概念对应的技术概念你该关心什么
请外部顾问闭源模型 API开箱即用、效果强;但数据要出内网,按 token 付费,版本由对方说了算
招正式员工入职开源权重本地部署数据不出内网、版本可锁定、可微调;但要自备显卡和运维
学历参数量(7B / 70B / 235B)相关但不决定一切,训练数据量与后训练质量同样关键
专业方向架构(Encoder / Decoder / Encoder-Decoder)决定它擅长理解还是擅长生成
一次能读完多厚的材料上下文长度(context length)直接决定能不能把整份文档塞进去问
全公司统一的入职手续OpenAI 兼容协议换人不用改流程——换模型不用改业务代码
让他做一套你的题用自己的测试集实测唯一能拍板的依据,榜单只能用来圈候选
⛔ 整讲只有一条铁律 不存在「最好的模型」,只存在「在你的约束下最合适的模型」。 换一组业务约束,冠军就换人。所以任何人告诉你「现在最强的是 XX」,正确的反应都是问一句:在什么任务、什么预算、什么合规要求下?

01概念:三大架构分野

同一个 Transformer,拆开用还是整个用,决定了模型擅长什么

1.1 一切从 Transformer 开始

大模型本身基于 Transformer 架构。自 2017 年 Attention is all you need 诞生起,原始的 Transformer 模型为不同领域提供了灵感和启发。基于这个框架衍生出了一系列模型:有些只使用 encoder,有些只使用 decoder,有些同时使用 encoder + decoder

于是大模型的分类就有了三种:自编码模型(encoder)、自回归模型(decoder)和序列到序列模型(encoder-decoder)

图② 同一个 Transformer 拆出的三大架构
图② 同一个 Transformer 拆出的三大架构
类别代表用了什么基本原理擅长
自编码 AE
AutoEncoder
BERTEncoder-Only在输入中随机 MASK 掉一部分单词,根据上下文预测这个词内容理解类任务(NLU):情感分析、提取式问答、分类
自回归 AR
Autoregressive
GPTDecoder-Only从左往右学习,只能利用上文信息生成式任务(NLG):摘要、翻译、抽象问答,长文本生成能力强
序列到序列
Seq2Seq
T5Encoder-Decoder把每个 task 都视作序列到序列的转换既要理解又要生成的任务,如机器翻译

1.2 BERT:双向编码,擅长「读懂」

BERT 是 2018 年 10 月由 Google AI 研究院提出的预训练模型,全称 Bidirectional Encoder Representation from Transformers。它在机器阅读理解顶级水平测试 SQuAD1.1 中两个衡量指标上全面超越人类,并在 11 种不同 NLP 测试中创出 SOTA,包括将 GLUE 基准推高至 80.4%(绝对改进 7.6%)、MultiNLI 准确度达到 86.7%(绝对改进 5.6%)。

宏观上 BERT 分三个模块:最底层的 Embedding 模块、中间层的 Transformer 模块、最上层的预微调模块

模块内容
Embedding三种 Embedding 直接加和Token Embeddings(词嵌入,首个单词是 CLS 标志)、Segment Embeddings(句子分段嵌入)、Position Embeddings(位置编码)
双向 Transformer只使用经典 Transformer 的 Encoder 部分,完全舍弃 Decoder
预微调最后一层按任务调整。如句子级分类任务,直接取第一个 [CLS] token 的 final hidden state,加一层全连接后 softmax 预测标签

它的两大预训练任务也很有代表性:

1Masked LM

随机抽取 15% 的 token 参与 MASK 任务。其中 80%[MASK] 替换,10% 用随机单词替换,10% 保持不变。

2Next Sentence Prediction

输入句子对 (A, B),预测 B 是不是 A 的真实下一句。其中 50% 是真实的下一句(IsNext,正样本),50% 是随机抽取的句子(NotNext,负样本)。

BERT 的关键参数:transformer 层数 12、特征维度 768、head 数 12、总参数量 1.15 亿。数据集为 BooksCorpus(800M words)+ English Wikipedia(2,500M words)。

缺点也很明确:存在预训练与微调之间的输入噪声差异(训练时有 [MASK],实际使用时没有),而且更适合 NLU 任务,不适合 NLG

1.3 GPT:单向生成,擅长「往下写」

2018 年 6 月,OpenAI 发表论文《用生成式预训练提高模型的语言理解力》,推出了具有 1.17 亿参数的 GPT(Generative Pre-training)模型。

GPT 采用单向 Transformer:给定句子 [u1, u2, …, un],GPT 在预测单词 ui只会利用 [u1, …, u(i-1)] 的信息,而 BERT 会同时利用上下文 [u1,…,u(i-1), u(i+1),…,un]

GPT 的 Decoder Block 与经典 Transformer 的区别 GPT 取消了第二个 encoder-decoder attention 子层,只保留 Masked Multi-Head Attention 层和 Feed Forward 层。
另外,经典 Transformer 解码器用了 6 个 Decoder Block,GPT 用了 12 个

GPT 的训练分两阶段:① 无监督的预训练语言模型;② 有监督的下游任务 fine-tuning。预训练阶段最大化似然函数,本质是基于前 k 个词预测当前词,k 是上文窗口大小——理论上 k 取得越大,模型能获取的上文信息越充足,能力越强。

关键参数与 BERT 惊人地接近:层数 12、特征维度 768、head 数 12、总参数量 1.17 亿。数据集是 BooksCorpus,约 5 GB、7400w+ 句子、7000 本不同风格的书。选它的理由很实在:书籍文本包含大量高质量长句,保证模型学习长距离信息依赖;而且这些书未开源公布,下游数据集里很难见到,更能验证模型的泛化能力

缺点是:语言模型是单向的,且针对不同任务需要不同数据集分别微调,比较麻烦。

1.4 T5:把所有任务都变成 Text-to-Text

T5 由谷歌提出,目的是构建任务统一框架:将所有 NLP 任务都视为文本转换任务。这样就能用同样的模型、同样的损失函数、同样的训练过程、同样的解码过程完成所有 NLP 任务。

结构与原始 Transformer 基本一致,改动有两处:① 简化版 Layer Normalization,去除了 bias,并把 Layer Norm 放在残差连接外面;② 简化版相对位置编码——每个位置编码是一个标量,被加到 logits 上用于计算注意力权重,各层共享位置编码,但同一层内不同注意力头的位置编码独立学习

预训练任务包括两种类型:Causal Language Modeling(因果语言建模)和填空任务(Masked Language Modeling)。数据集是对 Common Crawl 过滤后得到的 C4(the Colossal Clean Crawled Corpus),去掉重复的、低质量的、看着像代码的文本,只保留英文。

关键参数:层数 24、特征维度 768、head 数 12、总参数量 2.2 亿

1.5 为什么最后是 Decoder-only 赢了

今天你能叫得出名字的大模型,几乎清一色是 Decoder-only。原因有两层:

理论双向注意力的低秩问题

Encoder 的双向注意力会存在低秩问题,这可能会削弱模型表达能力。就生成任务而言,引入双向注意力并无实质好处

工程同等成本下的最优解

Encoder-Decoder 架构在某些场景下表现更好,大概只是因为它多了一倍参数。所以在同等参数量、同等推理成本下,Decoder-only 架构就是最优选择。

✅ 和上一讲的铁律接上了 Decoder-only 干的事,正是上一讲那条铁律:根据已有的上文预测下一个词。ChatGLM、LLaMA、BLOOM、Baichuan、Qwen 的训练目标全都是这一句话。
所以下一节看各家模型时,你会发现它们的训练目标完全一样,差别全在「零件选型」上——用哪种归一化、哪种激活函数、哪种位置编码。

02原理:主流开源模型差在哪

训练目标全都一样,差别在四个零件的选型上

2.1 先记住这张零件表

看模型不要从名字看,要从四个零件看。几乎每一份模型技术报告的「模型结构」一节,讲的都是这四件事怎么选:

零件它解决什么常见选项
归一化 normalization让深层网络训练稳定、不发散Post-LN / Pre-LNLayerNorm / RMSNorm / DeepNorm
激活函数 activation提供非线性表达能力ReLU / GeLU / GeGLU / SwiGLU
位置编码 position告诉模型词的先后顺序,并决定能不能外推到更长序列绝对位置编码 / RoPE(旋转) / ALiBi(相对)
注意力 attention决定推理速度与显存占用MHA / MQA(Multi-Query) / GQA(分组查询)

其中 RMSNormLayerNorm 的主要区别在于去掉了减去均值的部分,简化了计算,可以减少约 7%~64% 的计算时间。而 GQA 则是通过减少键值(KV)缓存的使用来提升推理效率

图③ 读懂任何模型技术报告的四个零件
图③ 读懂任何模型技术报告的四个零件

2.2 五个代表模型的零件矩阵

把主流开源模型按这四个零件横向摆开,规律立刻显形:

模型架构归一化激活函数位置编码其他改动
ChatGLM-6BPrefix-DecoderPost-LN + DeepNormGeGLURoPEembedding 层梯度缩减(相当于缩小 10 倍),提升训练稳定性
LLaMADecoder-onlyPre-LN + RMSNormSwiGLURoPEBPE tokenizer,词表仅 32000
BLOOMDecoder-onlyPre-LN + embedding LNGeLUALiBiembedding 层后加 LN 使训练更稳定;ALiBi 外推性更好
Baichuan-7BDecoder-onlyPre-LN + RMSNormSwiGLURoPE与 LLaMA 一致的模型设计
QwenDecoder-onlyRMSNormSwiGLURoPEGQA 分组查询注意力,提升推理效率
这张表真正的看点 五个模型的训练目标完全相同——都是「根据已有的上文去预测下一个词」。差异全部集中在零件选型上,而且收敛趋势非常明显Pre-LN + RMSNorm + SwiGLU + RoPE 几乎成了事实标准,BLOOM 的 ALiBi 与 ChatGLM 的 GeGLU 是少数派。
所以读一个新模型的技术报告时,先定位这四个零件,十分钟就能判断它和你熟悉的模型差多远。

2.3 ChatGLM:中文友好的 Prefix-Decoder

ChatGLM-6B 是清华大学提出的开源、支持中英双语的对话语言模型,基于 General Language Model (GLM) 架构,具有 62 亿参数。它使用了和 ChatGPT 相似的技术,经过约 1T 标识符的中英双语训练(中英文比例 1:1),辅以监督微调、反馈自助、人类反馈强化学习。

它的训练目标与别家不同,是基于自回归空白填充的:在输入文本中随机挖去一些连续的文本片段,然后训练模型按照任意顺序重建这些片段。GLM 把 NLU 任务转化为包含任务描述的完形填空问题,再通过自回归生成的方式来回答。

被称为 Prefix-Decoder 而不是纯 Decoder 的原因:Decoder 是单向的,但 GLM 某些位置可以看到双向——Part A 的词可以互相看到,但看不到 Part B;Part B 的词可以看到 Part A 和 Part B 中前面的词。

配置数值量化等级推理最低显存高效参数微调最低显存
参数6.2BFP16(无量化)13GB14GB
隐藏层维度4096INT810GB9GB
层数 / 注意力头数28 / 32INT46GB7GB
词表大小 / 最大长度130528 / 2048

优点是部署门槛低——INT4 精度下只需 6GB 显存,可以部署在消费级显卡上推理;缺点是模型容量小,记忆和语言能力相对较弱、多轮对话能力较弱

迭代也很典型:ChatGLM2-6B 采用 FlashAttention 把上下文从 2K 提到 32K,引入 Multi-Query Attention 使推理提速 42%,数学任务性能提升 571%ChatGLM3-6B 再加上多模态理解、代码增强模块与网络搜索增强。

2.4 LLaMA:英文世界的地基

LLaMA(Large Language Model Meta AI)由 Meta AI 于 2023 年发布,共有 7B、13B、33B、65B 四种版本。训练数据以英语为主的拉丁语系,外加来自 GitHub 的代码数据,不包含中韩日文,所有训练数据都是开源的。其中 65B 和 33B 在 1.4 万亿 token 上训练,7B 和 13B 在 1 万亿 token 上训练。

它最出名的结论是:130 亿参数的 LLaMA 在大多数基准上可以胜过 GPT-3(1750 亿参数),并且可以在单块 V100 GPU 上运行;最大的 650 亿参数版本可以媲美 Chinchilla-70B 和 PaLM-540B。

⚠️ LLaMA 系列的中文短板是结构性的 tokenizer 用 BPE,词表大小只有 32000,其中中文 token 只有几百个。后果是一个汉字常被切成多个 token——编码效率低、模型学习难度大,同样一句中文比英文消耗更多 token,既慢又贵。
社区的标准解法是扩充中文词表:在中文语料上用 Sentence Piece 训练一个中文 tokenizer(如 20000 个中文词汇),再与原始 LLaMA tokenizer 合并,得到词表大小 49953 的 Chinese LLaMA tokenizer。

迭代路线同样值得记:LLaMA 2 训练语料比 LLaMA 多 40%,上下文从 2048 升级到 4096,并训练出 chat 版本(SFT + RLHF);LLaMA 3 词表扩大到 128k,8B 和 70B 都采用 GQA 提升推理速度,预训练数据超过 15T token,比 LLaMA 2 大了 7 倍。

2.5 BLOOM 与 Baichuan:多语言路线与中文路线

BBLOOM · 多语言

由 Hugging Face 训练。训练数据包含英语、中文、法语等共 46 种语言13 种编程语言,1.5TB 去重清洗后的文本,中文语料占比 16.2%。参数规模有 560M、1.1B、1.7B、3B、7.1B 和 176B。词表大小 250880
176B 版本在 384 张 A100 80GB 上训练,2022 年 3 月至 7 月耗时约 3.5 个月,算力成本超过 300 万欧元
优点是多语言适应性好,可在多种语言间切换而无需重新训练。

CBaichuan · 中文

由百川智能于 2023 年 6 月发布,开放且可商用,支持中英双语,在约 1.2 万亿 token 上训练的 70 亿参数模型。词表大小 64000,最大长度 4096。模型设计与 LLaMA 一致。
在标准的中文和英文权威 benchmark(C-EVAL / MMLU)上均取得了同参数规模下的最好效果。
Baichuan-13B 参数达 130 亿、训练 1.4 万亿 token,改用 ALiBi 位置编码,并开源了 int8 和 int4 量化版本,可部署在 Nvidia 3090 这样的消费级显卡上。

2.6 Qwen:把后训练做厚

Qwen 由阿里巴巴训练并开源,最早于 2023 年 8 月开源 70 亿参数规模,随后陆续开源多个版本,参数从 18 亿到 720 亿。结构上是 GQA + RMSNorm + SwiGLU + RoPE 的组合。

以 Qwen2.5-7B-Instruct 为例:参数 7B、隐藏层维度 3584、层数 28、注意力头 28 个 query + 4 个 key-value(这就是 GQA:query 头多、KV 头少)、词表大小 151936、最大长度 32768

它的核心改进不在结构而在训练:预训练使用了高达 18 万亿 token 的数据(Qwen2 是 7 万亿),后训练阶段引入复杂的监督微调和多阶段强化学习,显著提升了长文本生成、结构化数据分析和指令遵循能力。

✅ 从这些迭代里能提炼出三条通用规律上下文长度一路暴涨:2048 → 4096 → 32K → 128K 甚至更长,因为它直接决定能不能把整份文档塞进去问。
词表大小是中文能力的先行指标:32000(LLaMA)→ 64000(Baichuan)→ 151936(Qwen)。词表越大,中文编码效率越高、越省 token。
训练数据量的增长快过参数量:1T → 1.4T → 15T → 18T。近几代的能力提升,更多来自数据与后训练,而不是单纯堆参数。

2.7 MoE:让参数「大而不贵」

近几年还多了一个必须知道的概念:MoE(Mixture-of-Experts,混合专家)。它把前馈层拆成许多「专家」,每次推理只激活其中一小部分

所以 MoE 模型有两个参数量:总参数量决定显存占用,激活参数量决定推理计算量。以官方公布的 Qwen3 开源权重为例——Qwen3-235B-A22B 总参数 235B、激活参数 22BQwen3-30B-A3B 总参数 30B、激活参数 3B

⚠️ 别被 MoE 的总参数量骗了 看到「235B」就以为要按 235B 准备显卡是对的(权重都要加载),但以为它的推理速度等同于 235B 稠密模型就错了——实际参与计算的只有 22B
反过来,看到「激活 3B」就以为能力等同于 3B 稠密模型也错了。选型时这两个数字都要问清楚:一个决定你的显存账单,一个决定你的延迟。

2.8 显存账本:自建路线的第一道硬门槛

前面所有参数、架构、词表的讨论,到了自建部署这一步都要换算成同一件事:这张卡装不装得下。下面的算法十分粗糙,但足够在选型阶段筛掉一大半候选。

第一块:权重

显存占用的大头是权重,算法就一行:参数量 × 每个参数的字节数。精度决定后一个因子:FP32=4FP16/BF16=2INT8=1INT4=0.5

同一个 7B 模型(实测输出见 3.2 节,已含 1.2 倍开销系数):

精度权重含开销24GB 消费级显卡
FP3226.08 GB31.29 GB❌ 装不下
FP1613.04 GB15.65 GB✅ 装得下
INT86.52 GB7.82 GB✅ 宽裕
INT43.26 GB3.91 GB✅ 宽裕

这就是 2.3 节 ChatGLM-6B 那张表里「INT4 只需 6GB」的来源——量化是把大模型塞进小卡的第一手段,FP16 到 INT4 直接降到四分之一。代价是精度损失,具体损多少必须在你自己的任务上实测。

第二块:KV cache——长上下文的隐形成本

权重是固定的,KV cache 却随上下文长度线性增长。它缓存的是已生成 token 的 Key 和 Value,避免每生成一个词就重算一遍前文。

实测一下(28 层、head_dim=128、FP16、batch=1),对比 MHA(28 个 KV 头)与 GQA(4 个 KV 头):

上下文长度MHA(28 头)GQA(4 头)GQA 省了
2K0.77 GB0.11 GB86%
8K3.06 GB0.44 GB86%
32K12.25 GB1.75 GB86%
128K49.00 GB7.00 GB86%
✅ 这张表解释了两件事为什么所有新模型都换成了 GQA:128K 上下文下,MHA 的 KV cache 就要 49GB——比模型权重本身还大。换成 GQA 只剩 7GB,长上下文才变得现实。这也是 2.2 表里 Qwen 那一行「GQA 提升推理效率」的具体含义。
为什么并发一高就 OOM:KV cache 还要再乘 batch。单路 32K 只要 1.75GB,十路并发就是 17.5GB。压测时显存爆掉,十有八九是这里。

拆到多卡上:不是加卡就线性变快

FP16 下算一遍需要几张 80GB 卡:

规模权重含开销80GB 卡数
7B13.04 GB15.65 GB1 张
13B24.21 GB29.06 GB1 张
32B59.60 GB71.53 GB1 张(已经很满)
70B130.39 GB156.46 GB2 张
235B(MoE 总参数)437.72 GB525.27 GB7 张
⛔ MoE 的显存按总参数算,不按激活参数算 最后一行必须看清楚:Qwen3-235B-A22B 虽然只激活 22B,但 235B 的权重全部要加载进显存
MoE 省的是计算量(延迟、吐吐量),不是显存。 把「激活 22B」当成「22B 的显存需求」去报硬件预算,是自建路线上最昂贵的误判之一。

还有一件事要提前想:单卡装不下就要张量并行,而张量并行靠卡间通信。没有 NVLink 的机器上走 PCIe,通信开销可能把多卡的收益吞掉一大截——两张卡不等于两倍速度。所以实际选型时,能单卡装下的方案优先级往往高于参数更大但必须拆卡的方案。

03最小代码:先问服务「你到底有哪些模型」

列表里有 ≠ 你这一刻调得动

3.1 探测服务真实可用的模型

选型的第一步不是看榜单,是连上你要用的那个服务,把它真实支持的模型列出来,再逐个发一次最小请求。这两步缺一不可:

① 列举GET /v1/models 拿到声明支持的模型名
② 实测每个模型发一次 max_tokens=1 的请求
③ 核对看响应里的 model 字段是不是你请求的那个
probe_endpoint.py —— 探测 OpenAI 兼容服务的真实可用模型可直接运行
"""探测一个 OpenAI 兼容服务:它到底有哪些模型、哪个真能调通。

为什么要探测:模型列表里有,不代表你的账号这一刻调得动。
只有真发一次请求、看响应里的 model 字段,才算「调得通」。

运行前配置:
    export LLM_API_KEY="你的key"
    export LLM_BASE_URL="https://api.deepseek.com"
依赖:pip install "openai>=1.0"
"""
import os
import sys

from openai import OpenAI

BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.deepseek.com")
client = OpenAI(api_key=os.environ["LLM_API_KEY"], base_url=BASE_URL, timeout=30.0)


def list_models():
    """第一步:把服务端声明支持的模型列出来。"""
    try:
        models = client.models.list()
    except Exception as err:
        print("[x] 列举模型失败:", type(err).__name__, err)
        return []
    names = sorted(m.id for m in models.data)
    print("服务 %s 声明了 %d 个模型:" % (BASE_URL, len(names)))
    for name in names:
        print("   ", name)
    return names


def probe(model):
    """第二步:真发一次最小请求,看它是否返回、返回的 model 是哪个。

    只要 1 个 token 的输出,成本可以忽略不计。
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": "hi"}],
            max_tokens=1,
        )
    except Exception as err:
        return False, "%s: %s" % (type(err).__name__, str(err)[:80])

    # 注意:响应里的 model 可能与你请求的名字不同(别名、灰度、降级都会改写它)
    served = response.model
    note = "实际服务模型 %s" % served
    if served != model:
        note += "(与请求名 %s 不一致)" % model
    return True, note


def main():
    names = sys.argv[1:] or list_models()
    if not names:
        print("没有可探测的模型名,可在命令行直接传入,如:")
        print("   python3 probe_endpoint.py deepseek-flash")
        return

    print("\n逐个实测(每个只花 1 个输出 token):")
    print("%-34s %-6s %s" % ("模型", "可用", "说明"))
    print("-" * 78)
    for name in names:
        ok, note = probe(name)
        print("%-34s %-6s %s" % (name, "✓" if ok else "✗", note))


if __name__ == "__main__":
    main()
⚠️ 为什么第②步不能省 /v1/models 返回的是服务端声明支持的清单,不代表你的账号、你的余额、你的地域在这一刻调得动它。常见情况:清单里有这个模型,但你的账号没开通权限、或后端没有可用容量,一调就报错。
只有真发一次请求、看到响应,才算「调得通」。 每个模型只花 1 个输出 token,成本可以忽略不计。
第③步在查一件很隐蔽的事 响应里的 response.model 可能与你请求的名字不同。别名、灰度发布、自动降级都会改写它——你以为在用旗舰版,实际被路由到了轻量版,效果对不上却查不出原因。
所以做模型对比实验时,务必记录响应里的实际 model 字段,而不是记录你请求时写的那个字符串。

运行方式:

probe_endpoint.py 的两种用法
$ export LLM_API_KEY="你的key"
$ export LLM_BASE_URL="https://api.deepseek.com"

# 用法一:不带参数 —— 先列举服务声明支持的模型,再逐个实测
$ python3 probe_endpoint.py

服务 https://api.deepseek.com 声明了 N 个模型:
    <模型名 1>
    <模型名 2>

逐个实测(每个只花 1 个输出 token):
模型                               可用   说明
------------------------------------------------------------------------------
<模型名 1>                         ✓     实际服务模型 <响应里的 model 字段>
<模型名 2>                         ✗     NotFoundError: ...

# 用法二:只测你关心的那几个,跳过列举
$ python3 probe_endpoint.py deepseek-flash deepseek-v4-pro

# 换一家服务,只改 base_url,脚本一个字都不用动
$ export LLM_BASE_URL="https://open.bigmodel.cn/api/paas/v4/"
$ python3 probe_endpoint.py

3.2 自建路线:先算显存,再谈效果

走 API 路线只需要探测模型列表;一旦要自建部署,第一个要算的不是效果而是显存。算不过来,后面的所有评测都是白做。把 2.8 节的两个公式写成脚本:

vram_estimate.py —— 权重与 KV cache 的显存估算可直接运行
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""显存估算——选型时最先要回答的问题:这张卡装不装得下。

只用标准库,直接 python3 vram_estimate.py 就能跑。
算的是量级,不是精确值;真实占用还受推理框架、批大小、显存碎片影响,
但用它筛掉明显装不下的候选,足够了。
"""

# 每个参数在不同精度下占多少字节
BYTES_PER_PARAM = {
    "FP32": 4,
    "FP16": 2,
    "BF16": 2,
    "INT8": 1,
    "INT4": 0.5,
}

# 推理时除权重外的额外开销系数(激活值、框架常驻、显存碎片)
# 经验值:按权重的 1.2 倍留余量
OVERHEAD = 1.2


def weight_gb(params_b, dtype):
    """权重占用(GB)。params_b 单位是十亿(B)。"""
    return params_b * 1e9 * BYTES_PER_PARAM[dtype] / (1024 ** 3)


def kv_cache_gb(layers, kv_heads, head_dim, seq_len, batch, dtype="FP16"):
    """KV cache 占用(GB)。

    每层要为 K 和 V 各存一份,所以乘 2。
    GQA 的关键就在 kv_heads——它比 query 头少,KV cache 直接按比例缩小。
    """
    per_token = 2 * layers * kv_heads * head_dim * BYTES_PER_PARAM[dtype]
    return per_token * seq_len * batch / (1024 ** 3)


def fits(need_gb, card_gb):
    return "✅ 装得下" if need_gb <= card_gb else "❌ 装不下"


def main():
    print("=" * 68)
    print("一、同一个 7B 模型,不同精度下的权重占用")
    print("=" * 68)
    print("%-8s %-12s %-14s %s" % ("精度", "权重(GB)", "含开销(GB)", "24GB 卡"))
    print("-" * 68)
    for dtype in ["FP32", "FP16", "INT8", "INT4"]:
        w = weight_gb(7, dtype)
        total = w * OVERHEAD
        print("%-8s %-12.2f %-14.2f %s" % (dtype, w, total, fits(total, 24)))
    print()
    print("结论:量化是把大模型塞进小卡的第一手段。")
    print("      FP16 到 INT4,显存直接降到四分之一。")
    print()

    print("=" * 68)
    print("二、不同规模模型在 FP16 下要几张 80GB 卡")
    print("=" * 68)
    print("%-16s %-14s %-12s %s" % ("模型规模", "权重(GB)", "含开销(GB)", "80GB 卡数"))
    print("-" * 68)
    for params in [7, 13, 32, 70, 235]:
        w = weight_gb(params, "FP16")
        total = w * OVERHEAD
        cards = int(total // 80) + (1 if total % 80 else 0)
        print("%-16s %-14.2f %-12.2f %d 张" % ("%dB" % params, w, total, cards))
    print()
    print("注意:235B 这一行对 MoE 模型同样成立——")
    print("      权重要全部加载,省的是计算量,不是显存。")
    print()

    print("=" * 68)
    print("三、KV cache:长上下文的隐形成本")
    print("=" * 68)
    # 参数取自公开的 7B 级模型常见配置
    layers, head_dim = 28, 128
    print("配置:28 层,head_dim=128,FP16,batch=1")
    print("%-14s %-12s %-14s %s" % ("上下文长度", "MHA(28头)", "GQA(4头)", "GQA 省了"))
    print("-" * 68)
    for seq in [2048, 8192, 32768, 131072]:
        mha = kv_cache_gb(layers, 28, head_dim, seq, 1)
        gqa = kv_cache_gb(layers, 4, head_dim, seq, 1)
        save = (1 - gqa / mha) * 100
        print("%-14s %-12.2f %-14.2f %.0f%%" % ("%d" % seq, mha, gqa, save))
    print()
    print("这就是 GQA 被所有新模型采用的原因:")
    print("  KV 头从 28 降到 4,KV cache 直接降到 1/7,长上下文才跑得起。")
    print()

    print("=" * 68)
    print("四、一个完整的选型判断示例")
    print("=" * 68)
    scenarios = [
        ("单张 4090 24GB 跑 7B", 7, "INT4", 24, 28, 4, 8192),
        ("单张 A100 80GB 跑 32B", 32, "FP16", 80, 64, 8, 8192),
        ("单张 A100 80GB 跑 70B", 70, "FP16", 80, 80, 8, 8192),
    ]
    for name, params, dtype, card, layers_n, kv_h, seq in scenarios:
        w = weight_gb(params, dtype) * OVERHEAD
        kv = kv_cache_gb(layers_n, kv_h, 128, seq, 1)
        total = w + kv
        print("%s:" % name)
        print("   权重+开销 %.1f GB + KV cache %.1f GB = %.1f GB  ->  %s"
              % (w, kv, total, fits(total, card)))
    print()
    print("70B FP16 单卡装不下,只有三条路:上多卡、上量化、换小模型。")
    print("选型阶段就要算清楚,不要等部署时才发现。")


if __name__ == "__main__":
    main()

运行输出

vram_estimate.py 的实际运行结果
====================================================================
一、同一个 7B 模型,不同精度下的权重占用
====================================================================
精度       权重(GB)       含开销(GB)        24GB 卡
--------------------------------------------------------------------
FP32     26.08        31.29          ❌ 装不下
FP16     13.04        15.65          ✅ 装得下
INT8     6.52         7.82           ✅ 装得下
INT4     3.26         3.91           ✅ 装得下

结论:量化是把大模型塞进小卡的第一手段。
      FP16 到 INT4,显存直接降到四分之一。

====================================================================
二、不同规模模型在 FP16 下要几张 80GB 卡
====================================================================
模型规模             权重(GB)         含开销(GB)      80GB 卡数
--------------------------------------------------------------------
7B               13.04          15.65        1 张
13B              24.21          29.06        1 张
32B              59.60          71.53        1 张
70B              130.39         156.46       2 张
235B             437.72         525.27       7 张

注意:235B 这一行对 MoE 模型同样成立——
      权重要全部加载,省的是计算量,不是显存。

====================================================================
三、KV cache:长上下文的隐形成本
====================================================================
配置:28 层,head_dim=128,FP16,batch=1
上下文长度          MHA(28头)     GQA(4头)        GQA 省了
--------------------------------------------------------------------
2048           0.77         0.11           86%
8192           3.06         0.44           86%
32768          12.25        1.75           86%
131072         49.00        7.00           86%

这就是 GQA 被所有新模型采用的原因:
  KV 头从 28 降到 4,KV cache 直接降到 1/7,长上下文才跑得起。

====================================================================
四、一个完整的选型判断示例
====================================================================
单张 4090 24GB 跑 7B:
   权重+开销 3.9 GB + KV cache 0.4 GB = 4.3 GB  ->  ✅ 装得下
单张 A100 80GB 跑 32B:
   权重+开销 71.5 GB + KV cache 2.0 GB = 73.5 GB  ->  ✅ 装得下
单张 A100 80GB 跑 70B:
   权重+开销 156.5 GB + KV cache 2.5 GB = 159.0 GB  ->  ❌ 装不下

70B FP16 单卡装不下,只有三条路:上多卡、上量化、换小模型。
选型阶段就要算清楚,不要等部署时才发现。
这份输出里最有决策价值的三行7B 在 FP32 下要 31GB——24GB 的 4090 装不下。很多人拿默认 FP32 加载完就 OOM,其实改成 FP16 就进去了。
32B FP16 要 71.5GB——在 80GB 卡上看似能进,但只剩 8GB 给 KV cache,并发稍高就爆。「刚好装得下」在生产环境里等于装不下。
128K 上下文的 MHA KV cache 要 49GB——比 7B 模型权重本身还大。选型时只看「支持 128K」而不算这笔账,上线必爆。
⚠️ 它算的是量级,不是精确值 真实占用还受推理框架(vLLM 的 PagedAttention 会明显降低 KV cache 碎片)、批大小、是否开启 prefix caching 影响。
它的用法是在选型阶段筛掉明显装不下的候选,而不是拿来写进采购单。真要定硬件,必须在目标框架上实测一轮。

04完整案例:把选型做成一张可复算的表

硬门槛先淘汰,加权排序再定名次

4.1 选型的两步法

「哪个模型更好」之所以答不了,是因为它把两类完全不同的条件混在了一起:

条件类型例子怎么处理
硬门槛数据不能出内网、必须支持 Function Call、必须能商用一票否决,不参与权衡。不满足就出局,再便宜也不要
软权重效果、价格、速度、可改造性按业务重要性给权重,加权求和排序

顺序不能反。只做加权排序是典型错误:一个效果不及格但极便宜的模型,会靠价格分把总分刷上去,排到第一名。

图④ 选型两步法:硬门槛淘汰在前,加权排序在后
图④ 选型两步法:硬门槛淘汰在前,加权排序在后
select_model.py —— 硬门槛 + 加权排序的选型脚本可直接运行
"""选型打分:把「哪个模型更合适」从拍脑袋变成一张可复算的表。

两步走,顺序不能反:
  第一步 硬门槛淘汰——不满足业务底线的直接出局,多便宜都不要;
  第二步 加权排序——在活下来的候选里,按业务权重算总分。

只做第二步是典型错误:一个效果不及格但极便宜的模型会把总分刷上去。
纯标准库,直接 python3 select_model.py 就能跑。
"""
# 候选模型的属性。分数一律 0~10,含义见 CRITERIA。
# ⚠️ 这些数字必须由你自己的实测填入,不要抄榜单。
CANDIDATES = [
    {"name": "闭源旗舰 API",   "quality": 10, "cost": 3, "latency": 6, "privacy": 2, "controllable": 3},
    {"name": "闭源轻量 API",   "quality": 7,  "cost": 8, "latency": 9, "privacy": 2, "controllable": 3},
    {"name": "开源权重 · 云端", "quality": 8,  "cost": 6, "latency": 7, "privacy": 5, "controllable": 8},
    {"name": "开源权重 · 内网", "quality": 7,  "cost": 5, "latency": 5, "privacy": 10, "controllable": 10},
    {"name": "小参数本地部署",  "quality": 4,  "cost": 10, "latency": 8, "privacy": 10, "controllable": 10},
]

CRITERIA = {
    "quality":      "任务效果:用你自己的测试集测出来的准确率/通过率",
    "cost":         "性价比:同样一批请求花多少钱,分越高越便宜",
    "latency":      "响应速度:首 token 延迟与整体吞吐",
    "privacy":      "数据可控:数据是否离开内网",
    "controllable": "可改造性:能不能微调、能不能锁版本",
}

# 每个场景两部分:gates 是一票否决的硬门槛,weights 是排序权重。
SCENARIOS = {
    "对外客服(量大、要便宜、要快)": {
        "gates": {"quality": 6},                 # 答错了要赔钱,效果有底线
        "weights": {"quality": 3, "cost": 5, "latency": 5, "privacy": 1, "controllable": 1},
    },
    "内部知识库(数据不能出内网)": {
        "gates": {"privacy": 8, "quality": 6},   # 合规是红线,不参与权衡
        "weights": {"quality": 3, "cost": 2, "latency": 2, "privacy": 5, "controllable": 4},
    },
    "复杂推理(效果压倒一切)": {
        "gates": {"quality": 8},
        "weights": {"quality": 8, "cost": 1, "latency": 1, "privacy": 1, "controllable": 1},
    },
}


def passes(candidate, gates):
    """硬门槛检查:返回 (是否通过, 被哪一项卡住)。"""
    for key, floor in gates.items():
        if candidate[key] < floor:
            return False, "%s=%d < %d" % (key, candidate[key], floor)
    return True, ""


def score(candidate, weights):
    """加权求和后归一化到 0~10,方便横向比较。"""
    total = sum(candidate[k] * w for k, w in weights.items())
    return total / sum(weights.values())


def main():
    print("评分维度:")
    for key, desc in CRITERIA.items():
        print("  %-13s %s" % (key, desc))

    for scenario, conf in SCENARIOS.items():
        gates, weights = conf["gates"], conf["weights"]
        print("\n" + "=" * 64)
        print("场景:%s" % scenario)
        print("硬门槛:%s" % "  ".join("%s>=%d" % (k, v) for k, v in gates.items()))
        print("权重:  %s" % "  ".join("%s=%d" % (k, v) for k, v in weights.items()))
        print("-" * 64)

        survivors, rejected = [], []
        for candidate in CANDIDATES:
            ok, reason = passes(candidate, gates)
            (survivors if ok else rejected).append((candidate, reason))

        for candidate, reason in rejected:
            print("  ✗ %-16s 出局(%s)" % (candidate["name"], reason))

        ranked = sorted(survivors, key=lambda pair: -score(pair[0], weights))
        for rank, (candidate, _) in enumerate(ranked, 1):
            print("  %d. %-16s 得分 %.2f" % (rank, candidate["name"],
                                            score(candidate, weights)))
        if ranked:
            print("  => 首选:%s" % ranked[0][0]["name"])
        else:
            print("  => 无人通过门槛,说明门槛定得过高或候选池太小")

    print("\n" + "=" * 64)
    print("同一批候选,换一组门槛和权重,冠军就换人——")
    print("所以「哪个模型最好」这个问题不成立,成立的是「在我的约束下哪个最好」。")


if __name__ == "__main__":
    main()

4.2 运行结果:换一组约束,冠军就换人

直接 python3 select_model.py,三个场景用的是同一批候选

select_model.py 的实际运行结果
评分维度:
  quality       任务效果:用你自己的测试集测出来的准确率/通过率
  cost          性价比:同样一批请求花多少钱,分越高越便宜
  latency       响应速度:首 token 延迟与整体吞吐
  privacy       数据可控:数据是否离开内网
  controllable  可改造性:能不能微调、能不能锁版本

================================================================
场景:对外客服(量大、要便宜、要快)
硬门槛:quality>=6
权重:  quality=3  cost=5  latency=5  privacy=1  controllable=1
----------------------------------------------------------------
  ✗ 小参数本地部署          出局(quality=4 < 6)
  1. 闭源轻量 API         得分 7.40
  2. 开源权重 · 云端        得分 6.80
  3. 开源权重 · 内网        得分 6.07
  4. 闭源旗舰 API         得分 5.33
  => 首选:闭源轻量 API

================================================================
场景:内部知识库(数据不能出内网)
硬门槛:privacy>=8  quality>=6
权重:  quality=3  cost=2  latency=2  privacy=5  controllable=4
----------------------------------------------------------------
  ✗ 闭源旗舰 API         出局(privacy=2 < 8)
  ✗ 闭源轻量 API         出局(privacy=2 < 8)
  ✗ 开源权重 · 云端        出局(privacy=5 < 8)
  ✗ 小参数本地部署          出局(quality=4 < 6)
  1. 开源权重 · 内网        得分 8.19
  => 首选:开源权重 · 内网

================================================================
场景:复杂推理(效果压倒一切)
硬门槛:quality>=8
权重:  quality=8  cost=1  latency=1  privacy=1  controllable=1
----------------------------------------------------------------
  ✗ 闭源轻量 API         出局(quality=7 < 8)
  ✗ 开源权重 · 内网        出局(quality=7 < 8)
  ✗ 小参数本地部署          出局(quality=4 < 8)
  1. 闭源旗舰 API         得分 7.83
  2. 开源权重 · 云端        得分 7.50
  => 首选:闭源旗舰 API

================================================================
同一批候选,换一组门槛和权重,冠军就换人——
所以「哪个模型最好」这个问题不成立,成立的是「在我的约束下哪个最好」。
场景硬门槛首选为什么
对外客服quality≥6闭源轻量 API小参数本地部署虽然最便宜,但效果分 4 分没过门槛直接出局
内部知识库privacy≥8, quality≥6开源权重 · 内网合规是红线,三个候选因数据要出内网被淘汰,只剩一个
复杂推理quality≥8闭源旗舰 API效果权重压倒一切时,贵和慢都可以忍
✅ 这个脚本真正的价值不在算法 加权求和谁都会写。它的价值在于逼你把「我觉得」变成一张能被同事质疑的表:权重是多少、分数从哪来、谁被什么条件淘汰,全部写在代码里,可复算、可争论、可存档。
⚠️ 但有一条必须说清:表里的分数必须来自你自己的实测。抄榜单填进去,得到的只是一个包装得很专业的拍脑袋。

4.3 参数量、上下文、价格:会变的数字怎么用

模型版本、价格、上下文长度这三类数字变化极快,任何写死在文档里的具体数值都有保质期。正确的用法是:记住量级和趋势,用到具体数字时现查官方页面

下面这组是从各家官方文档实际取回的样例,用来说明「一张选型表该记录哪些字段」,而不是让你背数字:

该记录的字段取自官方文档的样例为什么要记它
base_urlDeepSeek:https://api.deepseek.com;智谱:https://open.bigmodel.cn/api/paas/v4/决定你的客户端连到哪,换供应商只改这一处
模型名DeepSeek 文档当前列出 deepseek-flashdeepseek-v4-pro;智谱文档示例中出现 glm-5.3glm-5.3-flash模型名会改、会下线、会被别名转发,必须以官方页面为准
上下文长度 / 最大输出DeepSeek 两个模型均标注上下文 1M、最大输出 384K决定能不能把整份文档塞进去;最大输出常被忽略,长文生成会被它卡住
计价方式DeepSeek 按每百万 token 计价,且区分缓存命中 / 未命中高峰 / 低谷时段(低谷价为高峰价的一半)只比「单价」会算错账,缓存和时段能差出数倍
并发限制DeepSeek 文档为两个模型分别标注了并发上限压测前不看这个,上线必被限流打脸
开源许可Qwen3 六个 dense 模型以 Apache 2.0 开放权重能不能商用是硬门槛,不是软权重
⚠️ 一个真实存在的坑:API Key 与地域绑定 阿里云百炼的 OpenAI 兼容接口按地域提供 endpoint,并且 API Key 按地域绑定——用北京地域的 Key 去调弗吉尼亚地域的 endpoint,会返回 HTTP 401、错误信息 Incorrect API key provided、错误码 invalid_api_key
这个报错看起来像「Key 失效了」,实际是地域不匹配。不知道这件事的话,能查一整天。类似的坑各家都有,接入前先读一遍官方的「常见错误」章节,比事后 debug 便宜得多。

看懂计价结构,比比单价重要

选型时人人都会问「多少钱一百万 token」,但单一报价几乎不能用来估算真实账单。以 DeepSeek 官方定价页的结构为例,一份完整报价至少分四个维度:

维度含义对账单的影响
输入 vs 输出两者分开计价,输出通常明显贵于输入多轮对话把历史反复重传,吃的是输入配额;让模型少废话,省的是输出配额
缓存命中 vs 未命中相同前缀命中缓存时,输入价格大幅下降把固定的 system 提示词放在最前面且保持不变,就能持续命中缓存
高峰 vs 低谷时段不同时段适用不同单价批量、离线、可延迟的任务挪到低谷时段跑,成本差异可观
并发上限账号级别的速率限制不在报价表里,却直接决定你能不能撑住业务峰值
两个很容易搞反的概念上下文长度 ≠ 最大输出长度。 以 DeepSeek 官方文档为例,两者是两个独立的数(文档当前写的是上下文 1M、最大输出 384K)。能塞进去一百万 token,不代表能一口气吐出一百万。写长文时被截断,十有八九是撞上了最大输出而不是上下文。
同一家的不同型号价差很大。 各家都同时提供旗舰型与轻量型(如 DeepSeek 文档里列出的 deepseek-v4-prodeepseek-flash)。先用轻量型跑你的测试集,过不了再升级,而不是一上来就用旗舰型。
⛔ 关于具体数字的纪律 这一节所有数字都注明了出处,并且随时可能变化。写进你自己的技术方案时,请当场打开各家官方文档核对一遍,不要从任何二手资料(包括这一页)复制数字
选型方法论的保质期是几年,具体的模型名和价格,保质期可能只有几周。

05骨架模板:用自己的题去考候选模型

榜单只能圈候选,拍板必须靠你自己的测试集

4.2 节那张选型表里的分数从哪来?只有一个正当来源:拿你自己的题,让每个候选都做一遍。这份骨架把「跑一批题 → 记录耗时与 token → 判对错 → 出对比表 → 存档」串起来。

bench_skeleton.py —— 多模型横向实测骨架,只改 TODO 处可复用模板
"""选型实测骨架:拿自己的题目去考候选模型,出一张可复算的对比表。

榜单只能用来圈定候选,最终拍板必须靠你自己的测试集。
这份骨架把「跑一批题 → 记录耗时与 token → 判对错 → 出表」串起来。
复制后改 5 处 TODO。

运行前配置各家的 key,例如:
    export DEEPSEEK_API_KEY=...
依赖:pip install "openai>=1.0"
"""
import json
import os
import time

from openai import OpenAI

# TODO 1:登记参与对比的候选。base_url 与模型名以各家官方文档为准。
CANDIDATES = [
    {"label": "A 家轻量", "base_url": "https://api.deepseek.com",
     "key_env": "DEEPSEEK_API_KEY", "model": "deepseek-flash"},
    # {"label": "B 家旗舰", "base_url": "...", "key_env": "...", "model": "..."},
]

# TODO 2:换成你自己的题目。20~50 道覆盖真实业务分布,比 1000 道通用题有用。
TASKS = [
    {"id": "cls-1", "prompt": "判断这句话的情感,只回答 正面 或 负面:这家店服务真差。",
     "expect": "负面"},
    {"id": "cls-2", "prompt": "判断这句话的情感,只回答 正面 或 负面:物流很快,包装完好。",
     "expect": "正面"},
]

# TODO 3:换成你的判分逻辑。分类看精确匹配,生成类可换成 ROUGE 或人工抽检。
def judge(output, expect):
    return expect in output


def ask(candidate, prompt):
    """发一次请求,返回 (文本, 耗时秒, 输出token数)。失败返回文本 None。"""
    api_key = os.environ.get(candidate["key_env"])
    if not api_key:
        return None, 0.0, 0
    client = OpenAI(api_key=api_key, base_url=candidate["base_url"], timeout=60.0)

    start = time.time()
    try:
        response = client.chat.completions.create(
            model=candidate["model"],
            messages=[{"role": "user", "content": prompt}],
            # TODO 4:判定类任务把 temperature 压到 0,让结果可复现
            temperature=0,
            max_tokens=64,
        )
    except Exception as err:
        print("   [x] %s: %s" % (candidate["label"], type(err).__name__))
        return None, time.time() - start, 0

    elapsed = time.time() - start
    return (response.choices[0].message.content.strip(),
            elapsed,
            response.usage.completion_tokens)


def main():
    report = []
    for candidate in CANDIDATES:
        print("测试 %s%s)" % (candidate["label"], candidate["model"]))
        correct, total_time, total_tokens, done = 0, 0.0, 0, 0

        for task in TASKS:
            output, elapsed, tokens = ask(candidate, task["prompt"])
            total_time += elapsed
            total_tokens += tokens
            if output is None:
                continue
            done += 1
            ok = judge(output, task["expect"])
            correct += int(ok)
            print("   %-8s %s  %.2fs  %s" % (task["id"], "✓" if ok else "✗",
                                             elapsed, output[:24]))

        row = {
            "模型": candidate["label"],
            "完成数": done,
            "正确率": round(correct / done, 4) if done else None,
            "平均耗时秒": round(total_time / done, 3) if done else None,
            "输出token合计": total_tokens,
        }
        report.append(row)

    print("\n对比表:")
    if report:
        cols = list(report[0].keys())
        print("  " + "  ".join("%-12s" % c for c in cols))
        for row in report:
            print("  " + "  ".join("%-12s" % row[c] for c in cols))

    # TODO 5:落盘存档,下次换版本时直接对比,避免「感觉变差了」这种无证据判断
    with open("bench_report.json", "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    print("\n已写入 bench_report.json")


if __name__ == "__main__":
    main()
✅ 复制后你只需要改这五处 TODO 1 登记参与对比的候选(base_url / key 环境变量名 / 模型名)· TODO 2 换成你自己的题目 · TODO 3 换判分逻辑 · TODO 4 判定类任务把 temperature 压到 0 · TODO 5 结果落盘存档。其余代码不用动。

5.1 出题比写代码更重要

这个脚本的技术含量很低,难的是 TASKS 里放什么。三条经验:

原则说明
数量取 20~50 道覆盖真实业务分布的 20 道,比通用榜单的 1000 道有用得多。太少则噪声大,太多则迭代一次太慢
必须含难例边界把线上真实出过错的 case 收集进来。全是简单题的话,所有候选都满分,测了等于没测
答案要能机器判分分类、抽取、判断类任务天然可判;生成类任务先定好判分方式(ROUGE 或人工抽检比例),别测完了才发现没法打分

5.2 除了正确率,这三个数也要记

1耗时

平均耗时决定用户体感。有的模型正确率高 2 个点,但慢 3 倍,交互式场景直接出局。

2输出 token 数

同样答对,一个用 50 token、一个用 500 token,账单差 10 倍。啰嗦是要花钱的。

3失败次数

报错、超时、限流的比例。实验室里差别不大,上量之后它比正确率更能决定成败。

⚠️ 三个让实测结果失真的操作temperature 没压到 0:判定类任务每次结果都不一样,你会误以为是模型不稳,其实是自己没锁随机性。
只跑一次就下结论:网络抖动、服务端负载都会影响耗时。耗时至少跑三轮取中位数。
换了模型却没换提示词:不同模型对提示词的敏感度不同,用 A 调优过的提示词去测 B,对 B 不公平。要么都用中性提示词,要么各自调优后再比。
存档的意义:让「感觉变差了」变成可查的数字 bench_report.json 一定要存。云端模型会在你不知情的情况下更新版本,某天用户反馈「最近答得不如以前」时,有存档你就能跑同一套题直接对比,没存档就只能靠感觉争论。

5.3 许可证:真正的一票否决项

4.1 节把「商用许可」列为硬门槛,这里把它展开。许可证不参与加权打分——不满足就直接出局,再好的效果也没用。而它又恰恰是技术选型里最容易被跳过的一项。

许可类型典型约束实际影响
宽松开源许可
(如 Apache 2.0
保留版权声明即可自由使用、修改、分发、商用限制最少。Qwen3 的开源权重就采用 Apache 2.0
自定义社区许可厂商自拟条款,常附带用户规模上限、场景禁止项、品牌使用要求必须逐条读。「开源」两个字不等于可以随便商用
仅学术 / 需申请默认不开放商用,需邮件申请并获得官方书面授权审批周期不可控,不能排进紧急项目的关键路径
闭源 API只有使用权,受服务条款约束关注数据是否被用于训练、是否允许转售、是否允许蒸馏输出
⛔ 三个必须当场确认的问题学术开放 ≠ 可商用。 权重能下载、能跑通,和能不能用在收费产品里,是两件事。
模型许可 ≠ 数据许可。 微调用的数据集可能有自己的许可约束,有些明确禁止商用,也禁止用于训练商用模型。
许可会随版本变。 同一系列的不同尺寸、不同代次可能适用不同许可。以你要用的那个具体权重仓库里的 LICENSE 文件为准,不要拿系列印象代替。

落到操作上就一句话:把你要用的那个权重仓库的 LICENSE 和使用条款当场打开读一遍,截图存档,连同日期一起写进选型报告。这件事花不了十分钟,但漏了它,前面所有实测工作都可能作废。

06易错点汇总

按「架构 / 模型事实 / 选型 / 实测 / 部署」五类归并

⚠️ 一、架构概念

  • 把 BERT 当成能聊天的模型。 BERT 是 Encoder-Only 的自编码模型,靠 MASK 预测被遮住的词,适合理解类任务(分类、提取式问答),不适合 NLG。它不会「续写」。
  • 以为 GPT 的 Decoder 和经典 Transformer 的 Decoder 一样。 GPT 取消了第二个 encoder-decoder attention 子层,只保留 Masked Multi-Head Attention 和 Feed Forward。而且经典 Transformer 用 6 个 Decoder Block,GPT 用 12 个。
  • 把 ChatGLM 说成纯 Decoder-only。 它是 Prefix-Decoder:Part A 内部可以双向看到,Part B 只能看到 Part A 和自己前面的词。纯 Decoder 是完全单向的。
  • 把「Decoder-only 赢了」理解成「Encoder 没用」。 原因是双向注意力存在低秩问题可能削弱表达能力,且就生成任务而言双向注意力无实质好处;Encoder-Decoder 表现好往往只是因为多了一倍参数。在理解类任务上 Encoder 架构依然有价值。
  • 混淆 MLM 的 15% 与 80/10/10。 是随机抽 15% 的 token 参与 MASK 任务,在这 15% 里再按 80% 换 [MASK]、10% 换随机词、10% 保持不变 分配。

⚠️ 二、模型事实记混

  • 把参数量和训练数据量搞混。 ChatGLM-6B 是 62 亿参数、约 1T 训练数据;LLaMA-65B 和 33B 训练于 1.4T token,7B/13B 是 1T。两个数字不是一回事。
  • 以为参数越大中文越好。 LLaMA 的中文短板是词表结构性问题:词表仅 32000、中文 token 只有几百个,编码效率低。参数量再大也补不了这个,得扩充词表重训。
  • 忘了 BLOOM 用的是 ALiBi 而非 RoPE。 主流是 RoPE,BLOOM 是少数派,选它正是看中 ALiBi 外推性更好。ChatGLM 的激活函数是 GeGLU 而不是大多数模型的 SwiGLU,也是同类少数派。
  • 把 MoE 的总参数当成激活参数。 Qwen3-235B-A22B 是总参数 235B、激活 22B。总参数决定显存,激活参数决定计算量,两个都要问
  • 直接引用会过期的数字。 模型名、价格、上下文长度变化极快,写进方案前必须打开官方文档当场核对,不要从二手资料复制。

⚠️ 三、选型方法

  • 只做加权排序,不设硬门槛。 效果不及格但极便宜的模型会靠价格分刷到第一名。合规、商用许可、必备能力都是一票否决项,不参与权衡。
  • 拿榜单分数当选型依据。 榜单只能用来圈定候选。榜单题目与你的业务分布不同,还存在针对性优化的可能。
  • 问「哪个模型最好」。 这个问题不成立。成立的问法是「在我的任务、预算、合规要求下,哪个最合适」。
  • 把开源理解成免费。 开源省的是 API 费,但要付显卡、电费、运维、工程师时间。小规模用量下,闭源 API 往往更便宜。
  • 忽略最大输出长度。 上下文长度和最大输出是两个数。上下文 1M 不代表能一次吐出 1M,长文生成会被 max output 卡住。
  • 只比单价不看计价规则。 缓存命中与未命中、高峰与低谷时段的价格能差数倍,还要看并发上限。

⚠️ 四、实测环节

  • 只看模型列表就认为可用。 /v1/models 里有,不代表你的账号这一刻调得动。必须真发一次请求
  • 不核对响应里的 model 字段。 别名、灰度、降级都会改写它,你以为在测旗舰版,实际被路由到轻量版。对比实验要记录响应里的实际 model
  • temperature 没压到 0 就做判定类评测。 每次结果都不同,误判成模型不稳定。
  • 只跑一次就比耗时。 网络抖动和服务端负载影响很大,至少三轮取中位数。
  • 用为 A 调优过的提示词去测 B。 对 B 不公平,结论不可信。
  • 不存档实测结果。 云端模型会悄悄更新版本,没有存档就只能靠感觉争论「是不是变差了」。

⚠️ 五、接入与部署

  • API Key 与 endpoint 地域不匹配。 阿里云百炼的 Key 按地域绑定,跨地域调用返回 401 Incorrect API key provided、错误码 invalid_api_key——看着像 Key 失效,实际是地域错了。
  • 把显存需求算少了。 ChatGLM-6B 推理 FP16 需 13GB、INT8 需 10GB、INT4 只需 6GB。量化能大幅降低门槛,但要评估效果损失。
  • 忘了确认商用许可。 学术开放 ≠ 可商用。有的模型需要邮件申请并获得官方商用许可后才能免费商用。
  • 把 API-KEY 硬编码进源码。 一律走环境变量,且不要连同配置文件提交到 Git。
  • 把「学术开放」当成「可商用」。 权重能下载、能跑通,和能不能用在收费产品里是两件事。以你要用的那个具体权重仓库里的 LICENSE 为准,不要拿系列印象代替——同系列不同尺寸、不同代次可能适用不同许可。
  • 只看模型许可,忽略数据许可。 微调用的数据集有自己的许可约束,有些明确禁止商用、也禁止用于训练商用模型。
  • 把上下文长度当成最大输出长度。 两者是两个独立的数。能塞进去一百万 token,不代表能一口气吐出一百万——写长文被截断,十有八九是撞上了最大输出。

⚠️ 六、显存与容量测算

  • 只算权重,不算 KV cache。 权重是固定的,KV cache 随上下文长度和并发数线性增长。实测:28 层 MHA 模型在 128K 上下文下 KV cache 就要 49GB,比 7B 权重本身还大。
  • 把「刚好装得下」当成可以上线。 32B FP16 占 71.5GB,在 80GB 卡上只剩 8GB 给 KV cache,并发稍高就 OOM。生产环境要留出 KV cache 和并发的余量。
  • 默认用 FP32 加载。 7B 模型 FP32 要 31GB,24GB 卡直接 OOM;换 FP16 只要 15.65GB 就进去了。很多「显卡不够」其实是精度选错。
  • 以为加卡就线性变快。 单卡装不下就要张量并行,而张量并行靠卡间通信。没有 NVLink 时走 PCIe,通信开销会吞掉很大一截收益——两张卡不等于两倍速度
  • 拿粗算结果当采购依据。 估算只用来筛掉明显装不下的候选。真要定硬件,必须在目标推理框架(vLLM 等)上实测一轮,框架的显存管理策略差别很大。

07自测题

点击题目展开答案;能把这 14 题说清楚,这一讲就通了

一、三大架构
大模型的三种架构类别是什么?各自的代表模型与基本原理?

自编码模型 AE(Encoder-Only,代表 BERT):在输入中随机 MASK 掉一部分单词,根据上下文预测这个词,适合 NLU 理解类任务。
自回归模型 AR(Decoder-Only,代表 GPT):从左往右学习,只能利用上文信息,适合 NLG 生成类任务。
序列到序列模型(Encoder-Decoder,代表 T5):同时使用编码器和解码器,把每个 task 视作序列到序列的转换,适合机器翻译这类既要理解又要生成的任务。

BERT 的两大预训练任务是什么?MLM 的比例怎么分配?

MLM(Masked LM)NSP(Next Sentence Prediction)
MLM:随机抽取 15% 的 token 参与 MASK 任务,在这部分里 80%[MASK] 替换、10% 用随机单词替换、10% 保持不变。
NSP:输入句子对 (A,B) 预测 B 是否为 A 的真实下一句,50% 是真实下一句(IsNext 正样本),50% 是随机抽取的句子(NotNext 负样本)。

GPT 的 Decoder Block 与经典 Transformer 的 Decoder Block 有什么不同?

GPT 取消了第二个 encoder-decoder attention 子层,只保留 Masked Multi-Head Attention 层和 Feed Forward 层。另外经典 Transformer 解码器采用 6 个 Decoder Block,GPT 采用 12 个。GPT 总参数量 1.17 亿,BERT 是 1.15 亿。

为什么现在的大模型几乎都用 Decoder-only?

两个原因。理论上:Encoder 的双向注意力会存在低秩问题,可能削弱模型表达能力,而就生成任务而言引入双向注意力并无实质好处。工程上:Encoder-Decoder 在某些场景表现更好,大概只是因为多了一倍参数;在同等参数量、同等推理成本下,Decoder-only 就是最优选择。此外还有训练效率与工程实现上的优势。

二、主流模型
看一个新模型的结构,应该重点看哪四个零件?

归一化(Pre-LN/Post-LN,LayerNorm/RMSNorm/DeepNorm)、激活函数(ReLU/GeLU/GeGLU/SwiGLU)、位置编码(绝对/RoPE/ALiBi)、注意力机制(MHA/MQA/GQA)。
当前的事实标准是 Pre-LN + RMSNorm + SwiGLU + RoPE;BLOOM 用 ALiBi、ChatGLM 用 GeGLU 是少数派。RMSNorm 相比 LayerNorm 去掉了减去均值的部分,可减少约 7%~64% 的计算时间。

ChatGLM-6B 为什么被称为 Prefix-Decoder 而不是 Decoder-only?训练目标是什么?

因为 Decoder 是完全单向的,而 GLM 某些位置可以看到双向:Part A 的词可以互相看到但看不到 Part B,Part B 的词可以看到 Part A 和 Part B 中前面的词。
训练目标是基于自回归空白填充:在输入文本中随机挖去一些连续的文本片段,训练模型按照任意顺序重建这些片段

LLaMA 在中文上效果差的根本原因是什么?社区怎么解决?

根本原因是训练语料以英文为主、不包含中文,且 tokenizer 词表只有 32000,其中中文 token 只有几百个——一个汉字常被切成多个 token,编码效率低、模型学习难度大
标准解法是扩充中文词表:在中文语料上用 Sentence Piece 训练中文 tokenizer(如 20000 个中文词汇),与原始 tokenizer 合并,得到词表大小 49953 的 Chinese LLaMA tokenizer。

从 ChatGLM、LLaMA、Qwen 的迭代里能看出哪三条通用规律?

上下文长度一路暴涨:2048 → 4096 → 32K → 128K;② 词表大小是中文能力的先行指标:32000(LLaMA)→ 64000(Baichuan)→ 151936(Qwen);③ 训练数据量的增长快过参数量:1T → 1.4T → 15T → 18T,近几代能力提升更多来自数据与后训练而非堆参数。

三、选型与实测
选型为什么必须「硬门槛先淘汰、加权排序再定名次」?顺序反了会怎样?

因为合规、商用许可、必备能力这类条件是一票否决的,不能拿来和价格做权衡。顺序反了的典型后果是:一个效果不及格但极便宜的模型,靠价格分把总分刷上去排到第一名
运行 select_model.py 能看到:同一批候选,客服场景首选闭源轻量 API,内网知识库场景首选开源权重内网部署,复杂推理场景首选闭源旗舰 API——换一组约束,冠军就换人

为什么模型列表里有某个模型,还必须真发一次请求?响应里哪个字段最该核对?

因为 /v1/models 返回的只是服务端声明支持的清单,不代表你的账号、余额、地域在这一刻调得动它——可能没开通权限或后端无可用容量。
最该核对的是响应里的 model 字段:它可能与你请求的名字不同(别名、灰度发布、自动降级都会改写它)。做对比实验时要记录响应里的实际 model,而不是请求时写的字符串。

四、显存与部署
一个 7B 模型在 FP16 下大概要多少显存?怎么算?

算法就一行:参数量 × 每参数字节数。FP16 每参数 2 字节,所以 7e9 × 2 / 1024³ ≈ 13.04 GB;再乘上约 1.2 倍的激活值与框架开销,约 15.65 GB
对比一下:FP32 要 31.29 GB(这就是 24GB 卡常见的 OOM 原因),INT8 要 7.82 GB,INT4 只要 3.91 GB

为什么所有新模型都换成了 GQA?用数字说。

因为 KV cache 随上下文长度线性增长,而 KV 头数直接决定它的大小。实测(28 层、head_dim=128、FP16、batch=1):128K 上下文下,MHA(28 个 KV 头)要 49 GB,GQA(4 个 KV 头)只要 7 GB省 86%
49GB 比 7B 模型权重本身还大——不换 GQA,长上下文根本跑不起来。

Qwen3-235B-A22B 要按 235B 还是 22B 准备显存?为什么?

按 235B 准备显存。MoE 每次推理只激活一部分专家,但全部权重都要加载进显存——你不知道下一个 token 会路由到哪个专家。
FP16 下 235B 的权重约 437.72 GB,含开销约 525 GB,需要 7 张 80GB 卡
MoE 省的是计算量(延迟、吞吐),不是显存。把激活参数当显存需求去报硬件预算,是自建路线上最昂贵的误判之一。

32B FP16 算出来占 71.5GB,80GB 卡「装得下」——可以上线了吗?

不能。只剩 8GB 给 KV cache,单路 32K 上下文就要 1.75GB,十路并发直接 17.5GB——压测必 OOM。
「刚好装得下」在生产环境里等于装不下。还要注意:估算只用来筛掉明显装不下的候选,真要定硬件必须在目标推理框架(如 vLLM)上实测。

术语表

术语含义
AE / AutoEncoder自编码模型,Encoder-Only,代表 BERT;靠 MASK 预测被遮住的词,擅长理解类任务
AR / Autoregressive自回归模型,Decoder-Only,代表 GPT;从左往右预测下一个词,擅长生成类任务
Seq2Seq序列到序列模型,Encoder-Decoder,代表 T5;把所有任务视作文本到文本的转换
Prefix-Decoder前缀解码器;前缀部分可双向可见、生成部分单向,ChatGLM 采用
MLMMasked Language Modeling,带 mask 的语言模型训练任务
NSPNext Sentence Prediction,预测句子 B 是否为句子 A 的真实下一句
RMSNorm归一化方式,相比 LayerNorm 去掉了减去均值的部分,计算更省
DeepNorm用于超深网络的归一化方案,ChatGLM 采用 post layer norm 形式
SwiGLU / GeGLU / GeLU三种激活函数;SwiGLU 是当前主流选择
RoPE旋转位置编码,当前主流;提升模型对长序列的处理能力
ALiBi相对位置编码,外推性更好,BLOOM 与 Baichuan-13B 采用
GQAGrouped-Query Attention,分组查询注意力;通过减少 KV 缓存提升推理效率
MQAMulti-Query Attention,多查询注意力,ChatGLM2 用它提速 42%
MoEMixture-of-Experts 混合专家;有总参数量激活参数量两个数,前者定显存、后者定算力
BPEByte Pair Encoding,一种子词切分算法,用于训练 tokenizer
context length上下文长度,一次能处理的最大 token 数;与最大输出长度是两个不同的数
量化 / INT4 / INT8用更低精度存权重以降低显存门槛,代价是可能的效果损失
KV cache缓存已生成 token 的 Key/Value,避免重算前文;占用随上下文长度与并发数线性增长
FP32 / FP16 / BF16权重精度;每参数分别占 4 / 2 / 2 字节,直接决定显存占用
head_dim每个注意力头的维度;与层数、KV 头数一起决定 KV cache 大小
张量并行把单层权重切到多卡上并行计算;依赖卡间通信,无 NVLink 时收益明显打折
vLLM常用推理框架,以 PagedAttention 降低 KV cache 碎片、提升并发吞吐
激活参数量MoE 模型单次推理实际参与计算的参数量;它决定延迟,不决定显存(显存按总参数算)
权重开销系数估算显存时在权重之外预留的倍数(激活值、框架常驻、显存碎片),经验值约 1.2
C-EVAL / MMLU常用的中文 / 英文权威评测基准;只能用来圈定候选,不能代替自己的测试集
硬门槛选型第一步的一票否决项(合规、商用许可、效果底线),不参与加权打分
加权排序选型第二步,对通过硬门槛的候选按效果/价格/速度/可改造性打分排序
OpenAI 兼容协议以 OpenAI 的接口格式为事实标准,换供应商只需改 base_url 和模型名
最大输出长度单次响应能生成的最大 token 数;与上下文长度是两个独立的数,长文被截断通常撞的是它
前缀缓存相同请求前缀命中缓存时输入价格大幅下降;把固定 system 放最前面且保持不变就能持续命中
Apache 2.0宽松的开源许可,保留版权声明即可自由使用、修改、分发与商用;Qwen3 开源权重采用它
社区许可厂商自拟的许可条款,常附带用户规模上限与场景禁止项;开源 ≠ 可随意商用
✅ 一句话收束 这一讲给了你两样东西:一张读懂任何模型技术报告的零件表(归一化、激活、位置编码、注意力),和一套不靠榜单也能拍板的选型方法(硬门槛淘汰 + 加权排序 + 自己的测试集)。
知道选哪个之后,下一讲解决怎么把它接进代码里。