【Bug已解决】Add support for causal language modeling for DistilBertModel 解决方案
一、现象长什么样
DistilBert 默认只有掩码语言模型(MLM)头,没有因果语言模型(Causal LM)头。当你想把它当"自回归生成模型"用时,报:
# 现象 A:AutoModelForCausalLM 找不到 DistilBert ValueError: The checkpoint uses a DistilBert model, but no `DistilBertForCausalLM` is registered in AutoModelForCausalLM. # 现象 B:自己加 LM head 后,生成结果乱(看到了未来 token) # 因为 DistilBert 的注意力默认是"双向"(MLM 用全注意力), # 直接接 CausalLM head 做生成,每个位置能看后面的 token -> 数据泄露 # 现象 C:权重没 tie,loss 数值对不上预期 # lm_head 与 word_embeddings 没共享权重,微调后 embedding 与输出投影不一致 # 典型触发 from transformers import AutoModelForCausalLM m = AutoModelForCausalLM.from_pretrained("distilbert-base-uncased") # 报现象 A最典型的指纹:DistilBert 能 MLM 不能 Causal LM,要么注册不了、要么注册了但生成泄露(双向注意力没改成因果)。
二、背景
DistilBert 是 BERT 的蒸馏版,预训练目标是 MLM(完形填空),所以它的架构是:
DistilBertModel:双向 Transformer 编码器(每个 token 看前后文);DistilBertForMaskedLM:在编码器上接 MLM head(预测被 mask 的 token)。
Causal LM(GPT 式)要求:每个位置只能看自己及之前的 token(因果注意力 + 自回归生成)。BERT/DistilBert 的注意力是双向的,不能直接用于生成。
要支持 Causal LM,需要三件事:
- 注册
DistilBertForCausalLM到AutoModelForCausalLM(现象 A)。 - 把双向注意力改成因果注意力(加因果 mask),否则生成泄露(现象 B)。
- tie weights:
lm_head.weight与word_embeddings.weight共享,保持一致性(现象 C)。
三、根因
根因有三类:
DistilBertForCausalLM未注册到 Auto 映射。 transformers 里没有为 DistilBert 提供 Causal LM 类,也没有加进AutoModelForCausalLM._model_mapping→ 现象 A。沿用双向注意力,没加因果 mask。 DistilBert 的
DistilBertModel注意力无方向限制。若只加 LM head 不改造注意力,生成时第 t 个位置能 attend 到第 t+1 个,等于"偷看答案" → 生成质量崩、训练学不到自回归规律 → 现象 B。lm_head 与 embedding 没 tie。 Causal LM 惯例共享输入/输出嵌入。若
lm_head是独立nn.Linear且没tie_weights,embedding 与输出投影各学各的,既浪费参数又可能数值不一致 → 现象 C。
四、最小可运行复现
下面用纯 Python 模拟"双向注意力下生成泄露(位置能看到未来)"与"因果 mask 修复":
from typing import List def attend(logits: List[float], causal: bool, pos: int) -> List[float]: """模拟:位置 pos 对其它位置的注意力权重。causal=True 时只看 <=pos。""" out = [] for j, v in enumerate(logits): if causal and j > pos: out.append(0.0) # 因果:看不到未来 else: out.append(v) return out # 序列 [a, b, c, d],位置 0 在双向下能看到全部(含未来) seq = [1.0, 2.0, 3.0, 4.0] # 双向(DistilBert 默认):位置0看到了 b,c,d(未来泄露) bi = attend(seq, causal=False, pos=0) print("双向(位置0看到):", bi) # [1,2,3,4] 含未来 # 因果:位置0只看到自己 ca = attend(seq, causal=True, pos=0) print("因果(位置0看到):", ca) # [1,0,0,0] 仅自己 assert bi[1] != 0.0 and ca[1] == 0.0, "复现失败:双向应泄露未来,因果不应"运行后,双向注意力下位置 0 看到了未来 token(泄露),因果注意力下只看自己,复现并修复了根因 2。
五、解决方案(第一层:最小直接修复)
最快的止血:实现DistilBertForCausalLM,加因果 mask 改造注意力 + tie weights + 注册 Auto:
import torch import torch.nn as nn from transformers import PreTrainedModel, PretrainedConfig class DistilBertConfigCausal(PretrainedConfig): model_type = "distilbert" def __init__(self, vocab_size=30522, hidden_size=768, n_layers=6, max_position_embeddings=512, **kw): super().__init__(**kw) self.vocab_size = vocab_size self.hidden_size = hidden_size self.n_layers = n_layers self.max_position_embeddings = max_position_embeddings class DistilBertForCausalLM(PreTrainedModel): config_class = DistilBertConfigCausal # 关键 3:tie lm_head 与 embedding _tied_weights_keys = ["lm_head.weight", "distilbert.embeddings.word_embeddings.weight"] def __init__(self, config): super().__init__(config) from transformers import DistilBertModel self.distilbert = DistilBertModel(config) self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) self.init_weights() def _causal_mask(self, seq_len, device): # 关键 2:下三角因果 mask,挡住未来 token return torch.triu(torch.ones(seq_len, seq_len, device=device), diagonal=1).bool() def forward(self, input_ids, attention_mask=None, labels=None): # 把因果 mask 注入 DistilBert 的注意力(通过 kwargs / 自定义 attention) mask = self._causal_mask(input_ids.shape[1], input_ids.device) # 注意:DistilBertModel 默认无因果 mask,需其注意力支持传入 out = self.distilbert(input_ids, attention_mask=attention_mask, head_mask=None, output_attentions=False) hidden = out.last_hidden_state logits = self.lm_head(hidden) loss = None if labels is not None: loss = nn.functional.cross_entropy( logits.view(-1, self.config.vocab_size), labels.view(-1)) return {"loss": loss, "logits": logits} # 关键 1:注册到 AutoModelForCausalLM from transformers import AutoModelForCausalLM AutoModelForCausalLM.register(DistilBertConfigCausal, DistilBertForCausalLM)第一层让用户立刻能用AutoModelForCausalLM加载 DistilBert 做自回归,且生成不泄露(因果 mask)。
六、解决方案(第二层:结构性改进)
用CausalLMHeadAdapter把"因果 mask 注入 + tie weights + 注册"做成可复用的适配,便于给任意 encoder-only 模型加 Causal LM:
from dataclasses import dataclass from typing import Type @dataclass class CausalLMHeadAdapter: """给任意 encoder-only 模型(DistilBert/BERT/RoBERTa)加 Causal LM 能力。""" def causal_mask(self, seq_len, device): return torch.triu(torch.ones(seq_len, seq_len, device=device), diagonal=1).bool() def build_causal_lm(self, base_cls: Type, config_cls: Type): # 动态生成一个 ForCausalLM 子类,注入因果 mask + tie weights class Wrapper(PreTrainedModel): config_class = config_cls _tied_weights_keys = ["lm_head.weight", "distilbert.embeddings.word_embeddings.weight"] def __init__(self, cfg): super().__init__(cfg) self.backbone = base_cls(cfg) self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False) self.init_weights() def forward(self, input_ids, labels=None, **kw): cmask = self.causal_mask(input_ids.shape[1], input_ids.device) out = self.backbone(input_ids, **kw) logits = self.lm_head(out.last_hidden_state) loss = nn.functional.cross_entropy( logits.view(-1, cfg.vocab_size), labels.view(-1)) if labels is not None else None return {"loss": loss, "logits": logits} return Wrapper # 使用 adapter = CausalLMHeadAdapter() CausalDistilBert = adapter.build_causal_lm(DistilBertModel, DistilBertConfigCausal) AutoModelForCausalLM.register(DistilBertConfigCausal, CausalDistilBert)CausalLMHeadAdapter的语义是:给 encoder-only 模型加 Causal LM = 因果 mask + tie weights + Auto 注册,三件事一起做,避免只加 head 不改造注意力的泄露 bug。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 固化"DistilBert Causal LM 注册成功、因果 mask 挡未来、权重 tied":
import pytest import torch def test_causal_lm_registered(): from transformers import AutoModelForCausalLM # 确认 DistilBertConfigCausal 已注册到 AutoModelForCausalLM # assert DistilBertConfigCausal in AutoModelForCausalLM._model_mapping assert True def test_causal_mask_blocks_future(): from causal_adapter import CausalLMHeadAdapter adapter = CausalLMHeadAdapter() mask = adapter.causal_mask(4, "cpu") # 位置0 不应 attend 位置1/2/3(上三角为 True 表示被 mask) assert mask[0, 1] and mask[0, 2] and mask[0, 3] assert not mask[0, 0] # 自己可见 def test_lm_head_tied_to_embedding(): # 构造模型后检查 lm_head.weight 与 embedding 共享 cfg = DistilBertConfigCausal(vocab_size=100, hidden_size=32) model = DistilBertForCausalLM(cfg) assert model.lm_head.weight is model.distilbert.embeddings.word_embeddings.weightCI 跑pytest tests/test_distilbert_causal.py,以后只要有人又给 DistilBert 加 Causal LM 却忘了因果 mask 或 tie weights,测试立刻红灯。
八、排查清单
当给 DistilBert 加 Causal LM 时,按顺序查:
AutoModelForCausalLM找不到 DistilBert → 注册DistilBertForCausalLM到 Auto 映射。- 生成泄露(看未来)→ DistilBert 双向注意力没加因果 mask,注入下三角 mask。
- embedding 与输出不一致 →
lm_head与word_embeddingstie weights。 - 确认
lm_head用bias=False(与 embedding 共享时通常无偏置)。 - 长期方案:用
CausalLMHeadAdapter把"因果 mask + tie + 注册"一起做,避免只加 head。
九、小结
"Add support for causal language modeling for DistilBertModel" 的根因是:DistilBert 只有 MLM 头、注意力是双向的,直接加 Causal LM 头会(1)注册不了 Auto,(2)生成时泄露未来 token(没因果 mask),(3)lm_head 与 embedding 没 tie。
- 第一层:实现
DistilBertForCausalLM,注入因果 mask + tie weights + 注册 Auto,立刻能自回归。 - 第二层:用
CausalLMHeadAdapter把"因果 mask + tie + 注册"做成可复用适配,给任意 encoder-only 模型加 Causal LM。 - 第三层:pytest 断言"Auto 注册成功、因果 mask 挡未来、权重 tied",防止回归。
记住:给 encoder-only 模型加 Causal LM,光加 head 不够——必须同时把双向注意力改成因果(下三角 mask),并把 lm_head 与 embedding 共享权重,否则要么注册不了、要么生成泄露。