Transformers 中 CPMAnt 模型的接入、推理生成与架构解析:基于 cpmant 源码的完整实践指南
导读
本文围绕本仓库官方模型文档 docs/source/en/model_doc/cpmant.md 展开,系统讲解 CPMAnt——OpenBMB CPM-Live 开源训练项目的第一个里程碑模型——在 🤗 Transformers 中的使用方式与底层实现。你将掌握两种文本生成调用方式(Pipeline 与 CpmAntForCausalLM)、CpmAntConfig 全部超参数的语义与默认值、基于 rjieba + WordPiece 的中文分词细节,以及源码层面 RMSNorm、门控 FFN、分段位置编码与 prompt 前缀注入等架构机理。仓库中的实现文件与测试用例会作为每一条结论的落点依据。
1. CPMAnt 模型速览
CPMAnt 是一个约 10B 参数的开源中文预训练语言模型,由 OpenBMB 的 CPM-Live 开源持续训练项目发布,属于该项目产出的第一个里程碑模型。根据 cpmant.md 文档说明,该模型具有以下特点:
- 中文为主:面向中文文本生成设计;
- CUGE 基准表现:配合 delta tuning(增量微调)在 CUGE 中文评测基准上取得较好结果;
- 可压缩部署:官方还提供了适配不同硬件配置的压缩变体,供在资源受限环境下使用;
- 2023-04-12 由社区贡献接入 Transformers。
模型架构遵循“Pre-Norm + 残差”的 Transformer 解码器结构,但在位置编码、前馈网络与归一化上采用了更具现代性的设计(RMSNorm、门控 FFN、分段桶式相对位置编码),这些细节我们会在第 5 节结合源码逐一展开。
在 auto 模型映射 中,cpmant 同时注册了基础模型 CpmAntModel(映射关系见 modeling_auto.py)与自回归生成头模型 CpmAntForCausalLM,说明它可以被 AutoModelForCausalLM 等自动 API 以 "cpmant" 类型名识别加载。而本仓库内 cpmant 相关实现只包含四个文件,全部位于 src/transformers/models/cpmant/,即:
| 文件 | 职责 |
|---|---|
| configuration_cpmant.py | CpmAntConfig,全部超参数定义 |
| modeling_cpmant.py | PyTorch 模型实现(CpmAntModel、CpmAntForCausalLM 等) |
| tokenization_cpmant.py | CpmAntTokenizer 分词实现 |
| init.py | 惰性模块导出 |
对应测试位于 tests/models/cpmant/test_modeling_cpmant.py 与 tests/models/cpmant/test_tokenization_cpmant.py。
2. 快速上手:中文文本生成
原文档给出了两种完全对等的调用路径,二者都指向 Hugging Face Hub 上官方发布的检查点 openbmb/cpm-ant-10b。
2.1 方式一:通过 Pipeline 一行完成
pipeline 抽象了 tokenizer、模型、设备分配与解码细节,适合快速验证模型效果:
from transformers import pipeline
pipe = pipeline(
task="text-generation",
model="openbmb/cpm-ant-10b",
)
pipe("今天天气很好,")
task="text-generation" 会触发 TextGenerationPipeline,底层自动装配 CpmAntTokenizer 与 CpmAntForCausalLM。
2.2 方式二:通过 CpmAntForCausalLM 精细控制
需要显式控制设备映射、采样参数或反复解码时,建议直接使用模型类:
from transformers import CpmAntForCausalLM, CpmAntTokenizer
tokenizer = CpmAntTokenizer.from_pretrained("openbmb/cpm-ant-10b")
model = CpmAntForCausalLM.from_pretrained(
"openbmb/cpm-ant-10b",
device_map="auto",
)
input_ids = tokenizer("今天天气很好,", return_tensors="pt").to(model.device)
output = model.generate(**input_ids, max_new_tokens=50)
print(tokenizer.decode(output[0], skip_special_tokens=True))
要点说明:
device_map="auto"依赖accelerate,可在多 GPU / CPU offload 环境下自动分片放置这 10B 参数;- tokenizer 直接接受中文原始文本,内部先经结巴分词再走 WordPiece(详见第 4 节);
model.generate(**input_ids, max_new_tokens=50)表示在已有输入基础上续写最多 50 个新 token。模型会在解码时自动拼接并剥离内部使用的 prompt 前缀,用户侧无需关心。
在 modeling_cpmant.py 的 CpmAntForCausalLM.forward 文档字符串中还给出了一个实际生成示例:以 "今天天气不错," 为前缀,模型能自然续写出一段连贯的中文叙事,并正确使用换行 token </n> 作为句段分隔。这表明该模型对中文日常语料的续写质量可直接用于交互演示。
兼容性注意:模型文档与部分 docstring 中出现过
CPMAntTokenizer命名,本仓库当前实际导出的类名为CpmAntTokenizer,请以CpmAntTokenizer为准(见 tokenization_cpmant.py 的__all__)。
3. CpmAntConfig:模型结构与超参数总览
CpmAntConfig 定义于 configuration_cpmant.py,类型标记为 model_type = "cpmant"。openbmb/cpm-ant-10b 即采用默认值构建。下表整理全部超参数、默认值与语义,来自源码中的默认值字段:
| 参数 | 默认值 | 语义 |
|---|---|---|
vocab_size |
30720 | 词表大小 |
hidden_size |
4096 | 隐藏层维度 |
num_attention_heads |
32 | 注意力头数 |
dim_head |
128 | 每个注意力头的维度(num_heads * dim_head = hidden_size) |
dim_ff |
10240 | FFN 中间层维度 |
num_hidden_layers |
48 | Transformer 层数 |
dropout_p |
0.0 | 注意力与 FFN 内 dropout 概率 |
position_bias_num_buckets |
512 | 相对位置 bias 的桶(bucket)数量 |
position_bias_max_distance |
2048 | 模型可能遇到的最大序列长度(桶编码的上界) |
eps |
1e-6 | RMSNorm 的数值稳定项 |
init_std |
1.0 | 分段位置编码等参数初始化标准差 |
prompt_types |
32 | prompt 的类型数 |
prompt_length |
32 | 内部 prompt 前缀的长度 |
segment_types |
32 | 分段(segment)类型数 |
use_cache |
True | 是否缓存 KV,加速增量解码 |
tie_word_embeddings |
True | 是否绑定词嵌入权重 |
3.1 用配置初始化随机权重模型
CpmAntConfig 可以直接配合 CpmAntModel 构造一个未加载预训练权重的模型实例,常用于结构测试或从头训练:
from transformers import CpmAntModel, CpmAntConfig
# Initializing a CPMAnt cpm-ant-10b style configuration
configuration = CpmAntConfig()
# Initializing a model from the cpm-ant-10b style configuration
model = CpmAntModel(configuration)
# Accessing the model configuration
configuration = model.config
注意 CpmAntConfig 声明时带有 @strict(来自 huggingface_hub.dataclasses)修饰,即配置类采用严格数据类语义,传入未声明字段会报错,避免了拼写错误导致的静默失效。
3.2 几个超参数的源码级解读
prompt_types/prompt_length/segment_types:这三个参数不是普通的位置或 dropout 参数,而是直接决定嵌入表尺寸的“结构性”参数。由 modeling_cpmant.py 可见,输入嵌入层被定义为nn.Embedding(config.vocab_size + config.prompt_types * config.prompt_length, config.hidden_size)(即默认 30720 + 1024 = 31744 行),分段嵌入层为nn.Embedding(config.segment_types, config.hidden_size),并额外创建了CpmAntSegmentPositionEmbedding用于注入位置/分段信息。也就是说,只要修改这些配置,就必须配套重新训练或使用结构匹配的检查点。position_bias_num_buckets/position_bias_max_distance:控制相对位置“分桶”的粒度与覆盖范围。长距离位置被对数压缩到有限个桶中(算法见第 5.4 节),这使模型在2048默认上界内无需维护稠密的位置编码表。tie_word_embeddings=True与_tied_weights_keys = {}:虽然默认绑定词嵌入,但源码注释明确说明:lm_head其实是input_embedding.weight的一个“词表大小切片”(因为输入嵌入表多了 prompt 占位行),整张张量绑定会形状不匹配,因此检查点单独携带lm_head.weight,modeling_cpmant.py 中把_tied_weights_keys置空并由nn.Linear(config.hidden_size, config.vocab_size, bias=False)独立承载语言模型头。
4. CpmAntTokenizer:中文分词细节
CPMAnt 的词表面向中文,直接对中文字符做子词切分效率不高,因此 tokenization_cpmant.py 实现的 CpmAntTokenizer 采用“结巴分词(rjieba)→ 逐词 WordPiece”两级切分策略。
4.1 初始化与特殊 token
构造时所需文件为词表文件 vocab.txt(VOCAB_FILES_NAMES = {"vocab_file": "vocab.txt"}),可配置的特殊 token 如下(默认值均来自源码):
| 参数 | 默认值 | 含义 |
|---|---|---|
bod_token |
<d> |
文档起始(begin of document) |
eod_token |
</d> |
文档结束(end of document) |
bos_token |
<s> |
序列起始 |
eos_token |
</s> |
序列结束 |
pad_token |
<pad> |
填充 |
unk_token |
<unk> |
未知 token |
line_token |
</n> |
换行 |
space_token |
</_> |
空格 |
分词器的 padding_side 默认是 "left"(左侧填充),与自回归解码时“右侧续写”的语义吻合;model_input_names = ["input_ids", "attention_mask"] 表明它只输出这两类模型输入。
硬性依赖:__init__ 中调用 requires_backends(self, ["rjieba"]),即必须安装 rjieba 才能实例化该 tokenizer。rjieba 是 C++ 结巴分词的高性能 Python 绑定,缺失时会抛出带明确提示的 ImportError。安装方式为常规 pip 安装 rjieba 后重启解释器即可。
4.2 两级分词流程
def _tokenize(self, text):
"""Tokenize a string."""
output_tokens = []
for x in rjieba.cut(text, False):
output_tokens.extend(self.wordpiece_tokenizer.tokenize(x))
return output_tokens
即 rjieba.cut(text, False) 先把中文文本切分为词/词组,随后内嵌的 WordpieceTokenizer(tokenization_cpmant.py)对每个词执行最长匹配子词切分:
- 若整词在词表中,直接作为一个 token;
- 否则逐步缩短尾部前缀(
end -= 1循环)寻找最长可匹配子串; - 单字长度超过
max_input_chars_per_word(默认 200)或无可匹配子串时回退为<unk>。
4.3 特殊 token 的“透明化”处理
一个易被忽略的实现细节:加载词表后,tokenizer 把词表中的 </_>(空格)与 </n>(换行)这两个特殊词条的 id 直接复用给普通字符 " " 与 "\n"(见 __init__ 中 self.encoder[" "] = self.encoder[space_token] 一段),随后从词表中删除这两个特殊 token 并将它们从 added_tokens 中弹出。效果是:用户在显式控制分词时看到的空格/换行就是普通 token 而非特殊 token,解码时也不会残留多余特殊标记。save_vocabulary 保存时会做逆变换,把 " "/"\n" 还原为 </_>/</n> 写回文件,保证可往返。
另外,_decode 在解码前会过滤掉 pad_token_id、eos_token_id 与 bos_token_id,避免生成结果尾部出现大量填充与结束标记——这也是 generate 后需要配合 skip_special_tokens=True 的一个内在原因。
4.4 分词正确性的测试证据
tests/models/cpmant/test_tokenization_cpmant.py 中有针对中文分词的精确断言,例如给定 "今天天气真好!" 期望得到 rjieba 分词结果 ["今天", "天气", "真", "好", "!"],并验证了 token-id 互转与文本重建的一致性。这些测试用 @require_rjieba 标记,说明整条测试链路都建立在 rjieba 可用之上。
5. 架构与源码级原理:CpmAntModel 内部分解
模型实现集中在 modeling_cpmant.py(共 783 行)。它并非简单复刻某一经典结构,而是融合了 RMSNorm、门控 FFN、分段相对位置偏置与 prompt 前缀机制。逐层自底向上看:
5.1 RMSNorm:无偏置的层归一化
CpmAntLayerNorm(modeling_cpmant.py)实现的是 RMS Layer Normalization:计算隐藏状态的均方根作为归一化分母(torch.rsqrt(variance + eps)),只在 float32 下统计方差以避免低精度溢出,权重 weight 初始化为 1(由 _init_weights 用 init.ones_ 完成)。相比带偏置的 LayerNorm,RMSNorm 参数更少且训练更稳定,这也是现代 LLM 的常见选择。
5.2 带残差的 Pre-LN 块
每个 CpmAntTransformerBlock(modeling_cpmant.py)由自注意力子块与 FFN 子块串联,且均采用 Post-Residual / Pre-LN 形式:
CpmAntSelfAttentionBlock:先对输入做layernorm_before_attention,注意力输出经 dropout 后加回原输入(残差);CpmAntFFNBlock:同样先layernorm_before_ffn,再进入 FFN 并残差相加。
注意力实现 CpmAntAttention 中 Q/K/V 均使用 bias=False 的线性投影,点积注意力分数除以 sqrt(dim_head) 后直接加上 position_bias(而非常见的旋转位置编码或绝对位置加和),并用 Bool 型 attention mask 将无效位置填充为 -inf(softmax 前)与 0(softmax 后)。当 past_key_values 非空时调用 past_key_values.update(...) 追加新 KV,len_k 随之增长,从而支持增量解码。
5.3 门控 GELU 前馈网络
FFN 并非单一线性层,而是 CpmAntDenseGatedACT + 输出投影的两段式:
gate_score = self.act(self.w_0(hidden_states)) # GELU(w_0 x)
hidden_states = self.w_1(hidden_states) # w_1 x
hidden_states = gate_score * hidden_states # 逐元素门控相乘
即 hidden = GELU(x·W0) ⊙ (x·W1),随后经 w_out 投影回 hidden_size。这种“门控线性单元”结构(类 GLU 变体)相比普通 ReLU FFN 有更强的特征选择能力。CpmAntDenseGatedACT、CpmAntIntermediate、CpmAntOutput 中虽有部分类名与 BERT 一致,但真正被 TransformerBlock 串联使用的是上述 self_att + ffn(注意 CpmAntIntermediate 与 CpmAntOutput 仅以 Copied from ... Bert 注释保留,实际不参与 cpmant 主路径计算)。
5.4 分段 + 相对位置的“双桶”编码:CpmAntSegmentPositionEmbedding
这是 CPMAnt 区别于一般 Transformer 最核心的设计,实现在 modeling_cpmant.py:
- 分段桶(segment bucket):
_segment_relative_position_bucket计算query_segment * num_segments + key_segment,即用 (q 段, k 段) 组合编号描述跨段关系,再整体平移+num_buckets; - 绝对位置桶(absolute bucket):
_position_bucket采用 T5 式分桶——短距离线性精确映射(max_exact = num_buckets // 4内),长距离取绝对值后用对数函数压缩映射到剩余桶,符号方向仅保留正负两档(源码注释always bidirectional in CPMAnt),因此同一表格可复用前向/后向; - 合并规则:当
key_segment == query_segment(同段内)时选用绝对位置桶,跨段时选用分段桶——torch.where完成二选一; - 可学习偏置表
relative_attention_bias形状为(segment_types^2 + position_bias_num_buckets, num_attention_heads)(默认(32*32+512, 32)),按桶号用F.embedding查表得到(batch, num_heads, len_q, len_k)的偏置张量,并在注意力内与点积分数相加。
由于偏置表行数 = segment_types * segment_types + position_bias_num_buckets,这解释了为何 position_bias_num_buckets 默认 512、segment_types 默认 32:二者共同决定位置编码参数量。
5.5 CpmAntModel.forward:prompt 前缀的自动拼接与剥离
解码器入口(modeling_cpmant.py)的 forward 做了几件对用户“透明”的事:
- 自动判断分段:
segment = torch.where(input_ids != 0, 2, 0),非零 token 归为段 2,pad 归为段 0; - 自动拼接 prompt:在真实输入头部拼上
prompt_length(默认 32)个 id 位于[vocab_size + 2*prompt_length, vocab_size + 3*prompt_length)区间的“提示 token”,并将这些位置的 segment 置 0,作为固定引导前缀; - 构造自定义注意力掩码:
_prepare_attention_mask中context[:, None, :] | (context[:, :, None].logical_not() & directional_mask_2d)实现“前序上下文可见 + 因果三角”的复合掩码,再约束span相等并对左侧 padding 做遮蔽;directional_mask_2d即下三角因果掩码; - 缓存分支:
use_cache为真时创建DynamicCache;当存在past_length(第二轮起)时,只取每轮最新的 1 个位置的 segment/hidden 参与计算,并在切片后通过attention_mask[:, past_length:]、position_bias[:, :, past_length:]对齐; - 输出剥离 prompt:首次前向结束后,将 hidden states、attentions、hidden_states 序列的前
prompt_length位裁掉(hidden_states[:, self.prompt_length:, :]),保证对外暴露的last_hidden_state与输入严格对齐,用户无需感知内部 prompt。
对外返回标准的 BaseModelOutputWithPast(last_hidden_state、past_key_values、hidden_states、attentions 四件套),与其余 Transformers 解码器模型接口一致。
6. CpmAntForCausalLM:生成与训练
6.1 结构
CpmAntForCausalLM(modeling_cpmant.py)继承 CpmAntPreTrainedModel 与 GenerationMixin:
self.cpmant = CpmAntModel(config)
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
get_input_embeddings/set_input_embeddings 均代理到底层 self.cpmant.input_embedding,方便 LoRA、prefix tuning 等增量训练框架替换嵌入。prepare_inputs_for_generation 的注释指出:由于 forward 内部会自己裁掉已缓存的头部序列,生成时需要把完整的 input_ids 传入。
6.2 前向计算与训练损失
forward 依次完成:self.cpmant(...) 得到末层隐藏状态 → self.lm_head 计算 logits → 若传入 labels 则用 CrossEntropyLoss 计算语言建模损失。同时支持 logits_to_keep 参数,只对序列最后若干个位置计算 logits,从而大幅减少训练/评估时的显存占用。
一个完整的监督微调片段(构造 labels 对齐示例):
from transformers import CpmAntForCausalLM, CpmAntTokenizer
tokenizer = CpmAntTokenizer.from_pretrained("openbmb/cpm-ant-10b")
model = CpmAntForCausalLM.from_pretrained("openbmb/cpm-ant-10b")
texts = "今天天气不错,"
labels = "阳光明媚,我和妈妈一起去超市买东西。"
inputs = tokenizer(texts, return_tensors="pt")
label_ids = tokenizer(labels, return_tensors="pt").input_ids
# 把标签拼接在输入之后作为模型输入
full_ids = torch.cat([inputs.input_ids, label_ids], dim=-1)
attention_mask = torch.ones_like(full_ids)
outputs = model(
input_ids=full_ids,
attention_mask=attention_mask,
labels=full_ids, # 结合你自己的目标掩码使用
)
loss = outputs.loss
实际生产微调中通常配合
Trainer/Seq2SeqTrainer与掩码策略(例如对前缀部分置-100不计算损失),此处仅演示前向与 loss 的接线方式。
6.3 测试覆盖
tests/models/cpmant/test_modeling_cpmant.py 以 CpmAntModelTester 为骨架,覆盖了:基础前向(test_cpmant_model)、LM head(test_cpmant_lm_head_model)、masked/因果两种推理一致性(test_inference_masked_lm / test_inference_causal)、单句与 batch 生成(test_simple_generation / test_batch_generation),以及保留梯度/注意力输出等通用行为。若想在本地改动源码后验证回归,可在仓库根目录运行:
pytest tests/models/cpmant/test_modeling_cpmant.py -q
(tokenizer 测试需先安装 rjieba,见上。)
7. 工程实践建议与边界提示
- 显存规划:
openbmb/cpm-ant-10b是约 10B 参数模型,直接加载默认权重将占用可观显存;文档建议的压缩变体与device_map="auto"是低资源环境的可行选项。具体量化/压缩后的指标以官方发布页与 Hub 检查点卡片为准。 - 依赖清单:模型推理依赖
torch;分词依赖rjieba(缺失时CpmAntTokenizer(...)直接报 ImportError);device_map="auto"依赖accelerate。多机多卡场景可复用仓库 examples/pytorch/language-modeling 下的训练脚本流程。 - 上下文长度:
position_bias_max_distance默认 2048,即默认检查点训练时相对位置桶覆盖到约 2048 的序列上界;超长输入需自行验证外推表现。 - API 兼容:本文所有类名与导入路径均以当前仓库 models/cpmant 目录为准;跨版本升级(尤其 v5 前后命名差异)请参考仓库根目录 MIGRATION_GUIDE_V5.md。
综上,CPMAnt 的接入点集中在 configuration_cpmant.py、modeling_cpmant.py 与 tokenization_cpmant.py 三份实现之中——理解“分词两级切分、输入 prompt 前缀、双桶分段位置编码、Pre-LN 门控 FFN 解码器”这四条主线,即可自由地在推理、微调与二次开发场景中驾驭这个 10B 中文模型。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00