系列 《读 Hugging Face 源码:从 from_pretrained 到 GRPO 的 loss》 第 5 / 5 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
四篇正文回答了一个问题:工具箱第五篇那六行代码,执行时各经过哪些文件的哪些函数。第一篇读模型的加载与一次前向,第二篇读 generate 的循环,第三篇读文本与数据两条流水线,第四篇读 LoRA 怎么挂上去与三种 loss 各在哪一行。本文把四篇压成一张「问题 → 文件 → 函数」的索引表,拎出贯穿五个库的四条设计线,再给一套自测。
读完这四篇,你应该能回答哪些问题?1 哪些文件名、函数名与数字必须能脱口而出?2 怎么判断自己是”读过”还是”掌握”了?3
先把整个系列放在一张图上——箭头是调用或数据上的依赖,不是阅读顺序:
%%{init: {"flowchart": {"wrappingWidth": 230}}}%%
%% 图:四篇的依赖:03 造 input_ids / labels 与 batch,01 的模型算前向与 ForCausalLMLoss,02 的 generate 反复调 01 的 forward,04 的 peft 改 01 的 nn.Linear、trl 用 03 的 apply_chat_template 与 map 造 labels、用 01 的 loss_function 算 SFT loss
flowchart TB
T3["03 tokenizers · datasets<br/>tokenizer.json 五段 · chat template · Arrow · map · collate_fn"]
T1["01 transformers 模型侧<br/>from_pretrained 六步 · DecoderLayer · attention 注册表 · KV cache · ForCausalLMLoss"]
T2["02 generate<br/>GenerationConfig · LogitsProcessor · StoppingCriteria · _sample 循环"]
T4["04 peft · trl<br/>inject_adapter · lora.Linear · build_labels · dpo_loss · _compute_loss"]
T3 -- "input_ids · labels · attention_mask" --> T1
T1 -- "forward(past_key_values) 每步一次" --> T2
T4 -- "setattr 换掉 nn.Linear" --> T1
T3 -- "apply_chat_template · map" --> T4
T1 -- "model.loss_function" --> T4
一、总览:系列回答的问题与主线
系列的一句话主张是:这几个库的骨架是「注册表 + 字符串键」,读源码就是找到注册表、找到 compute_loss、找到 forward 里那一行调用,往下追。model_type 选模型类、_attn_implementation 选 attention 函数、loss_type 选 loss、target_modules 选要换的层——每一处都是一张表和一个键,模型代码不改,实现随便换。
| 篇 | 回答的问题 | 一句话结论 | 必记的文件 / 函数 / 数字 |
|---|---|---|---|
| 01 模型侧 | 三个文件怎么变成 nn.Module,forward 怎么走到 loss |
加载六步:读 model_type → 查表选类 → meta device 建骨架 → mmap 按名字填 → 收尾;前向逐行对应 L4 结构图,attention 与 loss 都是注册表调用 |
modeling_auto.py 的 _LazyAutoMapping;torch.device("meta");safe_open;ALL_ATTENTION_FUNCTIONS;ForCausalLMLoss;sdpa/eager 差 \(10^{-4}\)(fp32)与 1.0(bf16);Loading weights 290/290 |
| 02 generate | 一次采样的完整调用链 | 四段:GenerationConfig 三层优先级 → 按非空字段装配 processor 串 → 停止条件 → _sample 循环,每步六件事 |
_prepare_generation_config;_get_logits_processor(processor 在前、warper 在后);TemperatureLogitsWarper / TopK / TopP;unfinished_sequences;不传长度时 Qwen2.5 生成到 2048 |
| 03 tokenizers · datasets | 从 messages 到 input_ids,从 Arrow 文件到 collate_fn |
tokenizer.json 一个 JSON 描述五段 Rust 流水线,Python 只是壳;Dataset 是 Arrow 表的 mmap 视图,map 写新文件、文件名是 fingerprint,shuffle 只改索引 |
normalizer → pre_tokenizer → model → post_processor → decoder;encode_batch;{% generation %};151665 / 151643 / 151936;cache-<fingerprint>.arrow;_indices |
| 04 peft · trl | LoRA 怎么挂上去,SFT / DPO / GRPO 的 loss 各在哪一行 | inject_adapter 用 setattr 把 nn.Linear 换成 lora.Linear,forward 多一行;SFT 的 -100 在 build_labels 那一次 map 里填;dpo_loss 十几行;GRPO 的优势、裁剪、KL、归一各一段 |
result + lora_B(lora_A(dropout(x))) * scaling;scaling = α/r;lora_B 全零;8.8M(1.8%);build_labels;-logsigmoid(β·Δ);(r − mean)/(std + 1e-4);loss_type 的 grpo / bnpo / dr_grpo |
1. 本文的章节安排
| 章 | 内容 |
|---|---|
| 二 | 逐篇回顾:每篇的骨架、关键代码位置与总纲提出的问题的答案 |
| 三 | 贯穿全系列的四条线:注册表、Rust / Arrow 内核、-100、disable_adapter |
| 四 | 常见误区 |
| 五 | 通关自测:判断与计算、跨篇综合、面试题、掌握判据 |
| 六、七 | 下一步与延伸阅读 |
二、逐篇回顾
1. 第一篇:transformers 模型侧——from_pretrained 怎么把三个文件变成 nn.Module,forward 怎么走到 loss
骨架。AutoModelForCausalLM.from_pretrained(name) 的六步:AutoConfig.from_pretrained 读 config.json 的 model_type;_LazyAutoMapping 用 (model_type, "AutoModelForCausalLM") 查 modeling_auto.py 里的表得到 Qwen2ForCausalLM——architectures 字段不参与选类;解析仓库里的 safetensors 分片;在 torch.device("meta") 下建骨架,不分配一字节权重;safe_open 拿到每个张量的切片句柄,赋值时才 mmap 读盘、按 state_dict 键名填;收尾 tie_weights、初始化缺失键、eval(),generation_config.json 也在此时读入。前向:Qwen2DecoderLayer 的 RMSNorm、apply_rotary_pos_emb(前后两半配对)、repeat_kv(GQA,expand 不拷内存)、SwiGLU MLP、两条残差;attention 那一行是 ALL_ATTENTION_FUNCTIONS[config._attn_implementation](...)——eager 是手算六步加 fp32 softmax,sdpa 是一次 scaled_dot_product_attention;KV cache 是 DynamicCache,每层 cat 到 key_states;loss 不在建模文件里,self.loss_function 按类名分派到 loss/loss_utils.py 的 ForCausalLMLoss。最后读 modular_qwen2.py:不到 200 行继承 Llama / Mistral,modular_model_converter.py 展开成自包含的 modeling_qwen2.py。
总纲的问题。8B bf16 加载峰值约等于 16 GB:meta device 骨架不占内存,权重 mmap 逐个赋进参数,不会同时存在「文件里一份 + 模型里一份」。sdpa 与 eager 在 fp32 下差 \(1.7 \times 10^{-4}\)、bf16 下差到 1.0:数学相同、求和顺序不同,bf16 只有 8 位尾数——比较两个 attention 实现的输出必须在 fp32。labels 传进去后:ForCausalLMLoss 先 shift(logits[:, :-1] 对 labels[:, 1:]),cross_entropy(ignore_index=-100) 只对非 -100 位置求和;num_items_in_batch 传入时分母从本 micro-batch 的有效 token 数换成整个梯度累积周期的总数,梯度累积才与大 batch 等价。
2. 第二篇:generate——一次采样的完整调用链
骨架。_prepare_generation_config:kwargs > 模型的 generation_config(来自仓库的 generation_config.json)> 全局默认;不认识的 kwargs 成为 model_kwargs 原样传给 forward,拼错的参数名由 _validate_model_kwargs 报错。_get_logits_processor 是 200 行的 if xxx is not None: append(...):先所有 processor(repetition penalty、n-gram、bad words、min length),do_sample 为真时再加 warper(temperature → top-k → top-p → min-p……),顺序由代码决定。_get_stopping_criteria:MaxLength、EosToken、StopString。get_generation_mode 选 greedy / sample / beam / assisted。_sample 的循环:_prefill 整段 prompt 前向一次(logits_to_keep=1)拿到 cache,之后每步 prepare_inputs_for_generation 只切最后一个 token → forward → 取 logits[:, -1] → 过 processor 串 → multinomial(softmax) 或 argmax → cat → unfinished_sequences &= ~stopping_criteria(...);停了的序列此后每步填 pad_token_id 但仍跟着前向——静态 batch,vLLM 的 continuous batching 解决的正是这个。流式输出是循环里多一个 streamer.put(next_tokens)。
总纲的问题。只传 temperature=0.2 不传 do_sample:Qwen2.5 的 generation_config.json 里 do_sample=False,走 greedy,warper 根本不装配,temperature 无效(新版本会打警告)。top_k=50, top_p=0.9:processor 串里 TopK 在 TopP 之前,先留 50 个再在这 50 个里按累积概率 0.9 截——结果是「先 k 后 p」的交集;换顺序结果一般不同,且 transformers 不允许用 logits_processor= 再传一个同类 warper(会报重复)。batch 里第 10 步停的那条:第 11–30 步 next_tokens 被 unfinished_sequences 掩成 pad_token_id,输出序列里这 20 位是 pad;它的前向没有省掉。
3. 第三篇:tokenizers 与 datasets——从 messages 到 input_ids,从 Arrow 文件到 collate_fn
骨架。tokenizer.json(Qwen2.5 的 7 MB)顶层九个键,后五个就是 Rust 流水线:normalizer(NFC)→ pre_tokenizer(GPT-2 正则切词 + ByteLevel)→ model(BPE,vocab 151643、merges 151387)→ post_processor(Qwen 不加特殊 token;Llama 的 TemplateProcessing 在此加 <s>)→ decoder。Tokenizer.from_file 读它就搭出整条流水线,Python、Node、Rust 共用。Python 壳 PreTrainedTokenizerFast:__call__ → encode_batch(进 Rust)→ BatchEncoding,padding 与 added_tokens 对齐在壳里做。apply_chat_template 是沙箱 Jinja 渲染 tokenizer_config.json 里的 chat_template;return_assistant_tokens_mask=True 依赖模板里的 {% generation %} 标记。load_dataset 找 builder → download_and_prepare → ~/.cache/…/no_robots-train.arrow;Dataset = MemoryMappedTable 视图 _data + 可选行索引 _indices + 格式;map 按 writer_batch_size 取批 → 调函数 → ArrowWriter 写 cache-<fingerprint>.arrow,fingerprint 由上一个 fingerprint、变换名、全部参数(函数用 dill 序列化)哈希;shuffle / select 只生成行号表;__getitem__ 切 Arrow → formatter → dict,变长 padding 在 DataLoader 的 collate_fn 里。
总纲的问题。三个数:151665 是 len(tok) = 基础词表 + added_tokens(22 个特殊 token);151643 是 vocab_size = BPE 表大小;151936 是 config.vocab_size = embedding 行数,为 GPU 对齐补到 128 的倍数——加 10 个特殊 token 只有 len(tok) 变,仍小于 151936,模型不用 resize_token_embeddings。"12345" 是 5 个 token:GPT-2 正则在 pre_tokenizer 里把数字单个切开,BPE 的合并只在「词」内进行;"hello" 是一个整词、merges 表里能合到底。改 print 后 10 分钟重跑:函数字节码变了,dill 哈希变,fingerprint 变,缓存文件名对不上——用 new_fingerprint= 固定,或把函数拆成不变的模块级函数。
4. 第四篇:peft 与 trl——LoRA 怎么挂上去,SFT / DPO / GRPO 的 loss 各在哪一行
骨架。peft:get_peft_model 返回 PeftModel(LoraModel(model)),原模型被原地改;_prepare_adapter_config 把 all-linear 展开成所有 nn.Linear 名(排除 lm_head);inject_adapter 用 named_modules 逐个匹配,命中的交给 _create_and_replace 新建 lora.Linear(base_layer, r, lora_alpha, ...) 再 setattr(parent, name, new_module);update_layer 建 lora_A = Linear(in, r)(kaiming)、lora_B = Linear(r, out)(全零)、scaling = α/r,_mark_only_adapters_as_trainable 冻结其余;forward 多一行 result + lora_B(lora_A(dropout(x))) * scaling,先算瘦的一半;merge 把 get_delta_weight 加进 base_layer.weight.data。trl:SFTTrainer._prepare_dataset 接受 messages / prompt-completion / 纯文本三种格式,套模板、tokenize、按 completion_mask 或 assistant_masks 在 build_labels 这一次 map 里造 labels(不该学的位 -100),可选 pack_dataset(bfd);DataCollatorForLanguageModeling 只 pad(input_ids 用 pad id、labels 用 -100),padding_free 时拼成一行加 position_ids 并把每段首 token 置 -100;compute_loss 走模型的 loss_function。DPOTrainer:concatenated_forward 把 chosen 与 rejected 拼成一个 batch 前向,取 completion 的 log p 之和;参考 log p 来自 ref_model、或 peft 时 disable_adapter() 下再前向一次、或 precompute_ref_log_probs;dpo_loss 的 sigmoid 分支是 -logsigmoid(β((logπ_c − logπref_c) − (logπ_r − logπref_r))),其余 loss_type 各改一行。GRPOTrainer:_generate_and_score_completions 每个 prompt 采 \(G\) 条、过 reward_funcs;advantages = (r − mean_group) / (std_group + 1e-4);_compute_loss:ratio = exp(logπ − logπ_old),-min(ratio·A, clip(ratio, 1−ε_low, 1+ε_high)·A),beta != 0 时加 β·KL(k3 估计),loss_type 决定按条平均(grpo)、全 token 平均(bnpo)还是除常数(dr_grpo)。
总纲的问题。target_modules=["q_proj", "v_proj"] 是「模块名以此结尾」的列表匹配,能命中 model.layers.3.self_attn.q_proj;字符串 "q_proj|v_proj" 被当作正则 re.fullmatch,要匹配完整路径必须写成 ".*\.(q_proj|v_proj)",原样写匹配不到。LoRA 反向仍经过冻结的 base_layer:梯度要从 loss 传到每一层的输入 \(x\),才能到达更靠前的 lora_A,base_layer 的 weight.grad 不算但 \(\partial L / \partial x = g W\) 必须算;省下的是优化器状态(AdamW 两份动量 × 全部参数)与权重梯度,激活内存不省。GRPO 一组 8 条全对:r − mean = 0,advantages 全零,per_token_loss 全零,这组对本步梯度零贡献;DAPO 的 dynamic sampling 把这样的组过滤掉、重采到 batch 满;beta=0 时 trl 不建参考模型、不算 per_token_kl。
三、贯穿全系列的几条线
1. 注册表 + 字符串键
model_type → 模型类(_LazyAutoMapping)、_attn_implementation → attention 函数(ALL_ATTENTION_FUNCTIONS)、类名 → loss 函数(LOSS_MAPPING)、loss_type → dpo_loss 的分支、target_modules → 要换的层。读任何一个新模型、新 Trainer,先找那张表。
2. Rust / Arrow 内核 + Python 壳
encode_batch 之下是 Rust,_data 之下是 Arrow 的 mmap。Python 源码读到这两条线就停:壳负责对齐、格式转换与缓存管理,速度与内存都由内核决定——batched=True 快一个量级是因为一次进 Rust 多线程处理一千条;shuffle 不拷数据是因为只改行号表。
3. -100 一个约定串起三个库
tokenizers 的 assistant mask({% generation %})→ trl 的 build_labels(mask 为 0 的位填 -100)与 collator(pad 位填 -100、padding_free 的段首填 -100)→ transformers 的 ForCausalLMLoss(ignore_index=-100)。三个库没有共享任何类,靠这个整数约定协作。
4. 一个上下文省一份模型
PeftModel.disable_adapter() 让同一个模型在 LoRA 关掉时就是参考模型:DPO 的 ref log p、GRPO 的 KL 都可以这样算,省掉一份完整权重;merge / unmerge 与之类似,改 weight.data 而不改结构。
| 概念 | 在哪一篇 | 与谁相连 |
|---|---|---|
_LazyAutoMapping / ALL_ATTENTION_FUNCTIONS / LOSS_MAPPING |
01 | 04 的 loss_type、target_modules 同一模式 |
generation_config.json |
01 读入、02 使用 | from_pretrained 收尾时读,_prepare_generation_config 用 |
past_key_values / DynamicCache |
01 建 | 02 的 prepare_inputs_for_generation 每步只喂一个 token 的前提 |
{% generation %} |
03 | 04 的 assistant_only_loss 报错的原因 |
map 与 fingerprint |
03 | 04 的 _prepare_dataset 就是几次 map |
-100 |
01 消费、03 产生、04 填写 | 见上文第 3 条 |
disable_adapter() |
04 | 02 的 generate 在 GRPO 采样时也走它 |
四、常见误区
| 误区 | 为什么错 | 正确说法 |
|---|---|---|
architectures 字段决定 from_pretrained 实例化哪个类 |
Auto 类只看 model_type 查自己那张表 |
AutoModelForCausalLM 对 model_type=qwen2 永远给 Qwen2ForCausalLM,architectures 只影响 AutoModel 的默认头 |
| 加载模型峰值内存是权重的两倍 | meta device 骨架不占内存,safetensors mmap 逐个赋值 | 峰值约等于权重本身 |
attn_implementation 换了结果就不一样、说明有 bug |
两种实现数学相同,差异来自求和顺序与精度 | fp32 差 \(10^{-4}\)、argmax 一致才是正常;bf16 下差 1.0 也正常 |
传了 temperature 就在采样 |
warper 只在 do_sample=True 时装配 |
看模型 generation_config.json 的 do_sample 默认值 |
top_k 与 top_p 的顺序看我传参的顺序 |
_get_logits_processor 按代码里 append 的顺序 |
固定 temperature → top-k → top-p |
len(tok) == config.vocab_size |
一个是 tokenizer 的词条数,一个是 embedding 行数 | 151665 ≠ 151936,后者为对齐补大;加少量 token 不需要 resize |
map 不会重跑,datasets 有缓存 |
缓存键是 fingerprint,函数体一变就失效 | 改任何一行(含 print)都重跑;用 new_fingerprint= 或模块级函数 |
shuffle 之后 map 应该一样快 |
shuffle 只改 _indices,之后是随机访问 Arrow |
先 map 再 shuffle,或 flatten_indices() |
target_modules="q_proj|v_proj" 能匹配到所有 q/v 层 |
字符串走 re.fullmatch,要匹配完整路径 |
用列表 ["q_proj", "v_proj"](后缀匹配)或写完整正则 |
| LoRA 训练显存约等于 1.8% 参数的显存 | 激活与反向的 \(gW\) 一点没省 | 省的是优化器状态与权重梯度 |
SFTTrainer 默认只对 assistant 算 loss |
默认 assistant_only_loss=False,全部非 pad 位都算 |
要 assistant-only 须模板带 {% generation %} 或用 prompt-completion 格式 |
| DPO 一定要加载两份模型 | peft 下 disable_adapter() 就是参考模型 |
一份权重、两次前向 |
| GRPO 一组全对是最好的样本 | 优势全零、梯度零贡献 | DAPO 过滤这类组重采;scale_rewards 与 loss_type 决定归一 |
五、通关自测
A. 判断与计算(10 题)
config.json里"model_type": "llama"、"architectures": ["MistralForCausalLM"],AutoModelForCausalLM.from_pretrained给出哪个类?- 70B bf16 模型、safetensors 分 30 片,
from_pretrained(device_map="auto")的 CPU 峰值内存约多少? attn_implementation="eager"与"sdpa"在 bf16 下同一输入的argmax不一致,是 bug 吗?labels长 \(T = 512\),前 200 位是-100,ForCausalLMLoss对多少个位置平均?- Qwen2.5-0.5B 上
model.generate(**inputs)不传任何长度参数,会生成多少个新 token? do_sample=True, top_k=0, top_p=1.0, temperature=1.0,processor 串里有几个 warper?"2024"在 Qwen2.5 的 tokenizer 里是几个 token?由五段中的哪一段决定?ds.map(fn, num_proc=8)跑完后把num_proc改成 4 再跑,命中缓存吗?- Qwen2.5-0.5B(hidden 896、24 层)上
r=16, target_modules="all-linear",lora_B初始化后第一步前向的输出与基座相比差多少? - GRPO
num_generations=8,一组 reward 为[1,1,1,1,0,0,0,0],scale_rewards="group",第一条的advantage是多少?
答案要点。(1)LlamaForCausalLM——只看 model_type。(2)约等于权重大小,device_map="auto" 下更少:meta 骨架 + 逐分片 mmap,不会 30 片同时驻留。(3)不是:bf16 8 位尾数,两种实现求和顺序不同,logits 差到 1.0 时 argmax 可能翻转;要比对请用 fp32。(4)312 个:shift 后目标是 labels[1:] 共 511 位,其中前 199 位是 -100,\(511 - 199 = 312\);-100 的位不进分母。(5)2048:仓库 generation_config.json 的 max_new_tokens=2048;配套脚本 5 个 token 的 prompt 输出总长 2053。(6)零个:top_k=0、top_p=1.0、temperature=1.0 都是「不装配」的默认值。(7)4 个:GPT-2 正则在 pre_tokenizer 把数字单个切开。(8)命中:num_proc 不进 fingerprint,结果文件由分片合并而来;改 fn 才失效。(9)零:lora_B 全零,\(B A x = 0\),第一步前向与基座逐位相同。(10)约 0.94:mean = 0.5,nanstd 是无偏标准差 \(\sqrt{8 \cdot 0.25 / 7} \approx 0.535\),`advantage = (1 - 0.5) / (0.535 + 10^{-4}) \approx 0.935\(;后四条为\)-0.935$$,一组之和为零。
B. 跨篇综合(5 题)
- 用 Qwen2.5 做
SFTTrainer(assistant_only_loss=True)报错。从 tokenizer 到 Trainer 追一遍错在哪个文件,两条修法各有什么代价? - 手写 decode 循环忘了传
past_key_values、只喂新 token,输出乱了。从第一篇的forward与第二篇的prepare_inputs_for_generation说明两处错。 - DPO 用 peft 训练时没有
ref_model。参考 log p 从哪来?涉及第四篇的哪个上下文与第一篇的哪一层? - 一次
SFTTrainer.train()中-100出现在哪三处、由哪三个库的哪三个函数写入或消费? - 换一个新模型架构到 transformers(假设已合入),
generate、get_peft_model、SFTTrainer分别需要它提供什么,才能不改一行代码直接用?
答案要点。(1)第三篇:chat_template_utils.py 检查模板里是否有 {% generation %},Qwen2.5 的模板没有,return_assistant_tokens_mask 报 ValueError;第四篇的 _prepare_dataset 传了这个参数。修法一:换一个带 {% generation %} 的模板(要保证与原模板渲染结果一致,否则分布漂移);修法二:改成 prompt-completion 格式走 completion_mask(多轮对话只能对最后一轮算 loss)。(2)第一处:没有 cache 时 forward 把这一个 token 当作位置 0 的完整序列,RoPE 位置错、attention 看不到历史;第二处:prepare_inputs_for_generation 只切最后一个 token 的前提是 past_key_values 里有之前所有层的 K/V——要么全序列重算,要么带 cache。(3)DPOTrainer 在 ref_model is None and is_peft_model 时用 with self.model.disable_adapter(): 再前向一次;disable_adapter 让每个 lora.Linear.forward 走 self.base_layer(x) 分支——第一篇里被 setattr 换掉的那些 nn.Linear。(4)trl build_labels(mask 为 0 的位)、trl DataCollatorForLanguageModeling(pad 位、padding_free 段首)、transformers ForCausalLMLoss(ignore_index=-100 消费);来源之一是 tokenizers 的 assistant mask。(5)generate:GenerationMixin 与 forward 接受 past_key_values / use_cache、返回 logits;get_peft_model:模块是 nn.Linear(或有对应 LoRA 层的类型)且命名可被 target_modules 匹配;SFTTrainer:forward(labels=...) 返回 loss(即走 loss_function),tokenizer 有 chat_template。
C. 面试题(6 题)
from_pretrained为什么快、内存为什么省?说出 meta device 与 safetensors mmap 各解决什么。generate里temperature、top_k、top_p各怎么实现?为什么顺序固定?tokenizer.json的五段各做什么?Llama 与 Qwen 在哪一段不同?datasets.map的缓存键是怎么算的?什么情况下会意外失效或意外命中?- LoRA 在源码层面是怎么「挂上去」的?训练时省的是什么、不省的是什么?
dpo_loss与 GRPO 的_compute_loss各是哪几行?loss_type在两处分别控制什么?
答案要点。(1)meta device:建骨架不分配权重内存;mmap:safe_open 的切片句柄赋值时按页读盘,峰值等于权重本身,且 290 个张量秒级加载。(2)TemperatureLogitsWarper 是 scores / temperature;TopK 用 topk 找第 \(k\) 大值,小于它的置 \(-\infty\);TopP 排序、softmax、cumsum,超过 \(p\) 的置 \(-\infty\)——各十行;顺序由 _get_logits_processor 里 append 的顺序决定,让 top-p 在 temperature 之后算才是「对调温后的分布截断」。(3)normalizer 规范化 Unicode;pre_tokenizer 切词 + 字节映射;model 做 BPE 合并;post_processor 加特殊 token;decoder 还原字节——Llama 的 post_processor 是 TemplateProcessing 加 <s>,Qwen 是 ByteLevel 不加。(4)update_fingerprint(old, "map", args),函数用 dill 序列化进哈希;改函数任何字节码、闭包变量、默认参数都失效;改 num_proc 或函数外的东西不失效;lambda 引用了可变全局变量时可能意外命中。(5)inject_adapter 匹配 target_modules,_create_and_replace 新建 lora.Linear(base_layer),setattr 换掉父模块属性;forward 多一行 \(\frac{\alpha}{r} B A x\);省优化器状态与权重梯度,不省激活与 \(\partial L / \partial x\)。(6)dpo_loss:logits = (logπ_c − logπref_c) − (logπ_r − logπref_r),sigmoid 分支 -logsigmoid(β·logits),loss_type 选公式变体(ipo、hinge…);GRPO:coef_1 = exp(logπ − logπ_old),coef_2 = clamp,-min(coef_1·A, coef_2·A),+ β·KL,loss_type 选归一方式(grpo 按条、bnpo 按 token、dr_grpo 除常数、dapo)。
D. 掌握判据
| 程度 | 判据 |
|---|---|
| 读过 | 能说出四篇各读了哪几个文件 |
| 理解 | A 组至少 8 题、B 组至少 4 题不看答案答对;能画出第一篇的六步与第二篇的四段 |
| 掌握 | 拿到一个没读过的 trl Trainer 或 transformers 新模型目录,30 分钟内找到它的 compute_loss / 注册表 / forward 里的注册调用,并说出与本系列哪一篇的骨架相同 |
六、下一步
本系列是《AI 算法工程师学习地图》 L4–L5 的深入篇。往前,它读的每一段代码都对应地图里的一篇公式或结构图:模型侧对应 L4 Transformer 系列第一篇,generate 对应 L0 第五篇第七章,LoRA 对应 L0 第三篇,三种 loss 对应后训练系列。往后,两条路:
- 推理侧。第二篇的
_sample是静态 batch、Python 循环、每层cat的 KV cache——vLLM 源码系列读 continuous batching 与 PagedAttention 怎么把这三件事各换掉一个。 - 训练侧。第四篇的
GRPOTrainer采样与训练在同一进程——verl 源码导读读 rollout 与 actor 分到不同 worker 之后同一条 GRPO loss 长什么样。
七、延伸阅读
本系列没有读的部分,需要时从下面进:
- transformers 的其余目录。
models/里 encoder、多模态(Qwen2VLForConditionalGeneration的pixel_values怎么进inputs_embeds)、integrations/(flash attention、bitsandbytes 量化的Linear4bit)、trainer.py(trl 的三个 Trainer 都继承它:训练循环、梯度累积、num_items_in_batch的来源)。 - accelerate。
device_map="auto"的切分与dispatch_model、多卡下Trainer的 DDP / FSDP 包装——Infra 地图的分布式训练系列。 - peft 的其他 tuner。
tuners/目录下 DoRA、AdaLoRA、IA3、prompt tuning,结构与lora/相同:一个Config、一个Model、一个Layer。 - trl 的其他 Trainer。
PPOTrainer(带 value head)、RewardTrainer、KTOTrainer、OnlineDPOTrainer,以及 GRPO 的 vLLM 采样后端(use_vllm=True,vllm_client.py)。 - datasets 的 streaming。
IterableDataset与iterable_dataset.py:不落 Arrow、边下边用,shuffle变成缓冲区随机——预训练规模的数据只能这样读。
版本与文件名以总纲所述为准;这几个库的目录结构一年内会变,读的方法不变。
-
五个:
from_pretrained怎么把三个文件变成nn.Module(六步);generate一步做哪六件事、processor 串怎么装配;tokenizer.json五段与map的 fingerprint;LoRA 怎么被setattr挂上去、forward多的那一行;SFT 的-100、dpo_loss、GRPO 的优势 / 裁剪 / 归一各在哪几行。 ↩ -
_LazyAutoMapping、torch.device("meta")、safe_open、ALL_ATTENTION_FUNCTIONS、ForCausalLMLoss;_get_logits_processor、unfinished_sequences;normalizer → pre_tokenizer → model → post_processor → decoder、151665 / 151643 / 151936、cache-<fingerprint>.arrow;inject_adapter、result + lora_B(lora_A(dropout(x))) * scaling、scaling = α/r、8.8M(1.8%);build_labels、-logsigmoid(β·Δ)、(r − mean) / (std + 1e-4)。 ↩ -
用第五章的三段自测:A 组 10 题判断与计算(至少 8 题)、B 组 5 题跨篇综合(至少 4 题)、C 组 6 题面试题(能不看答案讲两分钟);再按 D 表的「掌握」判据,拿一个没读过的 Trainer 试 30 分钟。 ↩
系列 《读 Hugging Face 源码:从 from_pretrained 到 GRPO 的 loss》 第 5 / 5 篇
系列总览 — 为什么这样组织、读它需要什么、读完能做什么
本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/hf-source-reading-series-recap-and-self-test.html)的前提下,欢迎各种形式的转载、翻译或商业引用。
COMMENTS
评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。