ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Transformers底层原理与生产避坑指南

Transformers底层原理与生产避坑指南

1. 为什么“调用预训练大模型”不等于“会用Transformers”?

我第一次在公司内部做技术分享时,被一位刚转行的同事当场问住:“老师,我照着Hugging Face官网代码跑通了BERT分类,但换到自己数据上准确率掉了一半——是不是模型没加载对?”
那一刻我才意识到:绝大多数人卡在的不是“怎么调用”,而是“调用时发生了什么”。

这本《Transformers 入门指南》不是教你怎么复制粘贴pipeline(),而是带你拆开那个黑盒——看清楚token是怎么被切开又重组的、attention权重如何在层间流动、为什么同样的模型在不同任务上要配不同的head结构、甚至为什么你model.eval()之后结果还是不稳定。

核心关键词就三个:Transformers、Python、预训练大模型。但它们的真实关系是:

  • Transformers是架构范式(不是某个库),它定义了“输入→分词→嵌入→多头注意力→前馈→输出”的完整数据流;
  • Python是实现载体,但关键不在语法本身,而在它如何承载PyTorch/TensorFlow的张量计算图;
  • 预训练大模型是现成的参数包,但它的价值只在你理解其设计边界后才能释放——比如BERT不能直接生成文本,而GPT类模型做分类必须加额外的pooling层。

我见过太多人把from transformers import AutoModel当成魔法咒语:

  • 有人用AutoTokenizer.from_pretrained("bert-base-chinese")处理英文文本,结果所有词都被切成了[UNK];
  • 有人把AutoModelForSequenceClassification直接喂给图像数据,报错信息里写着expected input shape (batch, seq_len)却还在查GPU显存;
  • 更常见的是——模型跑起来了,但model.config.hidden_size返回768,而下游任务需要1024维特征,没人去改config.json里的hidden_size字段。

这不是代码问题,是认知断层。
所以这篇指南的起点很明确:不教你“怎么跑通”,而教你“跑通时每一步在做什么”。
你会看到:

  • 分词器如何把“苹果手机”变成[101, 679, 3550, 102],又为什么"iPhone"会被切为["i", "##Phone"];
  • forward()函数里attention_mask和token_type_ids到底参与了哪些矩阵运算;
  • 为什么Trainer类默认关闭梯度裁剪,但在微调Llama时必须手动开启;
  • 甚至torch.compile()在A100上加速37%,但在V100上反而慢12%的底层原因。

这些细节不会出现在官方文档的Quick Start里,但它们决定了你能不能把模型真正用进生产系统。
如果你的目标是“能调用”,那网上有几百篇教程;
如果你的目标是“调用时不踩坑”,那请继续往下读——我们从最基础的tokenization本质开始。

2. 分词器不是翻译器:解剖Tokenizer的三重身份

很多人以为Tokenizer就是个“字符串→数字列表”的转换器,就像ord()函数一样简单。但当你发现"Hello, world!"被切成了['Hello', ',', 'world', '!'],而"HuggingFace"却变成['Hu', '##gging', '##Face']时,就会意识到:分词器本质上是一个基于统计规律的序列决策引擎。

2.1 WordPiece与Byte-Pair Encoding的底层逻辑差异

先看两个真实案例:

from transformers import AutoTokenizer # BERT用WordPiece tokenizer_bert = AutoTokenizer.from_pretrained("bert-base-uncased") print(tokenizer_bert.tokenize("unaffable")) # ['un', '##aff', '##able'] # GPT-2用Byte-Pair Encoding tokenizer_gpt2 = AutoTokenizer.from_pretrained("gpt2") print(tokenizer_gpt2.tokenize("unaffable")) # ['un', 'aff', 'able']

表面看都是切词,但机制完全不同:

  • WordPiece(BERT系):

    • 预先构建一个词表(vocabulary),大小固定(如BERT base是30522个token);
    • 切词时采用贪心算法:从最长子串开始匹配,匹配不到就切更短的,未登录词用##前缀标记子词;
    • 关键限制:词表外的字符永远无法被表示,比如中文古籍里的生僻字"龘"(音dá),BERT词表里没有,只能返回[UNK]。
  • Byte-Pair Encoding(GPT系):

    • 从单字节开始,统计相邻字节对出现频率,合并最高频的字节对,迭代生成词表;
    • 因此能覆盖所有Unicode字符——"龘"会被拆成UTF-8字节序列b'\xf0\xa9\xb6\xa1',再逐步合并成token;
    • 优势:零样本处理未知字符;代价:长文本token数可能比WordPiece多30%。

提示:选模型时务必确认分词器类型。比如你要处理含大量emoji的社交媒体文本,GPT类BPE分词器天然支持;但若需严格对齐中文词性(如“北京大学”必须作为一个整体token),则需用jieba+BERT的混合方案。

2.2 Tokenizer的三大隐藏组件:Normalizer、PreTokenizer、PostProcessor

官方文档只告诉你tokenizer.encode(),但实际调用链是:

input string → Normalizer(统一空格/标点/大小写) → PreTokenizer(按规则切分,如空格、标点) → Encoder(查词表映射ID) → PostProcessor(添加特殊token如[CLS]、[SEP],生成attention_mask)

以RobertaTokenizer为例,它的normalizer会把\u200b(零宽空格)直接删除,而BertTokenizer保留它——这导致同一段含零宽空格的文本,在两个模型里token数量不同,进而引发attention_mask长度不匹配错误。

实操验证:

from transformers import RobertaTokenizer, BertTokenizer text = "hello\u200bworld" roberta_tok = RobertaTokenizer.from_pretrained("roberta-base") bert_tok = BertTokenizer.from_pretrained("bert-base-uncased") print("Roberta:", roberta_tok.encode(text)) # [0, 3912, 1002, 2] print("Bert:", bert_tok.encode(text)) # [101, 7592, 2000, 1012, 102] # 注意:Bert多出一个token,因为\u200b被当作了独立字符

这就是为什么你在迁移模型时,经常遇到index out of range错误——不是代码写错了,是normalizer悄悄改了输入。

2.3 自定义分词器的实操陷阱:何时该重训?何时只需微调?

业务中常遇到两类需求:

  • 场景A:处理金融研报,术语如“QFII”、“ETF期权”频繁出现,但标准BERT词表里只有"Q","F","I","I"四个独立token;
  • 场景B:医疗文本含大量拉丁词根(如"cardio-","neuro-"),希望它们被识别为完整前缀而非碎片。

解决方案分三级:

  1. Level 1:扩充词表(最快,适用90%场景)

    # 加载原始tokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") # 添加新词(注意:必须用原始词表的编码方式) tokenizer.add_tokens(["QFII", "ETF期权"]) # 模型需同步扩展embedding层 model.resize_token_embeddings(len(tokenizer))
  2. Level 2:修改PreTokenizer规则(需懂正则)

    from tokenizers import Tokenizer, models, pre_tokenizers # 创建自定义tokenizer tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]")) tokenizer.pre_tokenizer = pre_tokenizers.Sequence([ pre_tokenizers.Whitespace(), # 先按空格切 pre_tokenizers.Digits(individual=True), # 数字单独切 pre_tokenizers.UnicodeScripts() # 按文字系统切(中/英/数分离) ])
  3. Level 3:重训整个分词器(仅当领域差异极大时)

    • 收集10万+行领域文本(如法律文书、医学论文);
    • 用tokenizers.train接口训练新词表;
    • 关键经验:词表大小必须与原模型一致(如BERT base必须30522),否则model.embeddings.word_embeddings维度不匹配。

注意:重训分词器后,config.json里的vocab_size字段必须手动更新,否则AutoModel.from_pretrained()会加载错误的embedding层。我曾因此调试三天,最后发现config.vocab_size还是30522,而新词表实际是30525。

3. 模型加载不是“开箱即用”:Config、State Dict与Dtype的三角约束

AutoModel.from_pretrained("bert-base-chinese")这行代码背后,至少发生三次关键校验。忽略任何一次,都会导致模型输出全为NaN或显存爆炸。

3.1 Config文件:模型的DNA说明书

每个预训练模型都附带config.json,它定义了模型的静态结构:

  • hidden_size: 隐层维度(BERT base=768,large=1024);
  • num_hidden_layers: Transformer层数(base=12,large=24);
  • num_attention_heads: 注意力头数(base=12,large=16);
  • intermediate_size: Feed-Forward层中间维度(base=3072,large=4096);
  • pad_token_id: 填充token的ID(必须与tokenizer的pad_token_id一致)。

最常被忽视的字段是torch_dtype:

{ "torch_dtype": "float32", "architectures": ["BertModel"], "hidden_size": 768, ... }

这个字段告诉AutoModel:加载权重时应转成什么精度。如果设为float32,而你用model.half()强制转float16,某些层(如LayerNorm)的epsilon值会因精度损失失效,导致梯度爆炸。

实测对比(A100 40GB):

dtype显存占用推理速度训练稳定性
float3212.4 GB1x稳定
float166.2 GB1.8x需GradScaler
bfloat166.2 GB1.9x原生支持(无需Scaler)

经验:微调时优先用bfloat16(PyTorch 1.10+),它保留与float32相同的指数位宽,避免float16的下溢问题。但transformers库默认不启用,需手动设置:

model = AutoModel.from_pretrained("bert-base-chinese", torch_dtype=torch.bfloat16)

3.2 State Dict:权重文件的物理存储格式

Hugging Face模型仓库里,pytorch_model.bin不是单一文件,而是分片存储的state dict。当你看到:

pytorch_model-00001-of-00003.bin pytorch_model-00002-of-00003.bin pytorch_model-00003-of-00003.bin

说明模型权重被按层切分(如Embedding层放第一片,Layer 0-3放第二片,Layer 4-11放第三片)。

为什么这么设计?

  • 下载加速:用户只需下载当前任务用到的层(如只用Embedding层做特征提取,可跳过Transformer层);
  • 内存优化:from_pretrained(..., low_cpu_mem_usage=True)会边下载边加载,避免全量读入内存;
  • 容错性:某一片损坏,其他片仍可加载。

但这也带来陷阱:

  • 如果你用torch.load("pytorch_model.bin")直接加载分片文件,会报KeyError: 'bert.embeddings.word_embeddings.weight'——因为分片文件只包含部分key;
  • 正确做法永远用AutoModel.from_pretrained(),它会自动拼接所有分片。

3.3 Dtype与Config的冲突:一个真实踩坑案例

某次部署时,客户要求用FP16节省显存。我执行:

model = AutoModel.from_pretrained("bert-base-chinese") model.half() # 转float16 tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese") inputs = tokenizer("hello", return_tensors="pt").to("cuda") outputs = model(**inputs) # 报错:RuntimeError: expected scalar type Float but found Half

排查发现:tokenizer返回的attention_mask是torch.int64,而model.half()后所有权重是torch.float16,但attention_mask参与的torch.where()运算要求dtype一致。

解决方案有二:

  1. 统一输入dtype(推荐):

    inputs = tokenizer("hello", return_tensors="pt") inputs = {k: v.to(torch.float16) if v.dtype == torch.int64 else v for k, v in inputs.items()} inputs = {k: v.to("cuda") for k, v in inputs.items()}
  2. 用AutoModel的dtype参数(一劳永逸):

    model = AutoModel.from_pretrained( "bert-base-chinese", torch_dtype=torch.float16, device_map="auto" # 自动分配GPU )

关键教训:model.half()只是改变模型权重dtype,不改变输入tensor的dtype。真正的dtype一致性,必须从tokenizer输出端就开始控制。

4. Pipeline不是银弹:从封装API到手动Forward的必经之路

pipeline()是Hugging Face最诱人的功能——三行代码搞定文本分类:

from transformers import pipeline classifier = pipeline("text-classification", model="distilbert-base-uncased-finetuned-sst-2-english") result = classifier("I love this movie!") # {'label': 'POSITIVE', 'score': 0.9998}

但它像一把瑞士军刀:好用,但想修精密仪器时,你得把它拆开。

4.1 Pipeline的四层封装:每一层都在隐藏什么?

pipeline()的调用链如下:

user input → Preprocess(tokenizer) → Model.forward() → Postprocess(logits→label)

问题在于:Preprocess和Postprocess是黑盒。当你发现classifier("I hate this movie!")返回{'label': 'POSITIVE', 'score': 0.51}时,根本不知道是tokenizer切错了,还是模型输出异常,还是postprocess的阈值设错了。

手动拆解等价代码:

from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased-finetuned-sst-2-english") model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased-finetuned-sst-2-english") # Step 1: Preprocess(显式控制) inputs = tokenizer( "I hate this movie!", return_tensors="pt", truncation=True, padding=True, max_length=512 ) # Step 2: Forward(可调试) with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits # [1, 2] probabilities = torch.nn.functional.softmax(logits, dim=-1) # Step 3: Postprocess(自定义逻辑) id2label = model.config.id2label # {0: 'NEGATIVE', 1: 'POSITIVE'} predicted_class = torch.argmax(probabilities, dim=-1).item() confidence = probabilities[0][predicted_class].item() print(f"Label: {id2label[predicted_class]}, Confidence: {confidence:.4f}") # Label: NEGATIVE, Confidence: 0.9997

现在你可以:

  • 检查inputs["input_ids"]是否合理(如[101, 1045, 2572, ...]);
  • 查看logits原始值(tensor([[ 2.1, -4.3]])),确认模型确实学到了负向倾向;
  • 修改id2label映射,适配自己的业务标签(如把"NEGATIVE"改成"差评")。

4.2 手动Forward的五个关键参数:attention_mask、token_type_ids、position_ids、head_mask、inputs_embeds

model(**inputs)看似简单,但inputs字典里至少包含5个张量。漏掉任何一个,都可能让模型失效:

参数作用必填性常见错误
input_idstoken ID序列✅用错tokenizer导致ID越界
attention_mask标记有效token(1)和padding(0)⚠️(多数情况必需)全1导致padding位置参与计算,输出异常
token_type_ids区分句子A/B(如NSP任务)⚠️(仅BERT系必需)RoBERTa不需要,但传入会报错
position_ids位置编码索引❌(自动推导)手动传入错误长度(如max_length=512但只传了10)
head_mask屏蔽特定注意力头❌(调试用)微调时误设导致梯度消失

实操验证attention_mask的重要性:

# 正确:mask掉padding inputs = tokenizer("hello", padding="max_length", max_length=10, return_tensors="pt") print("Correct mask:", inputs["attention_mask"]) # tensor([[1, 1, 1, 1, 1, 0, 0, 0, 0, 0]]) # 错误:全1 mask inputs_wrong = tokenizer("hello", return_tensors="pt") inputs_wrong["attention_mask"] = torch.ones_like(inputs_wrong["input_ids"]) outputs = model(**inputs_wrong) # 输出logits全为nan!

原因:padding位置的embedding是零向量,但attention_mask=1会让它们参与softmax计算,导致exp(0)/sum(exp(...))产生数值不稳定。

4.3 从Pipeline到Trainer:为什么微调必须放弃Pipeline?

pipeline()只适合推理,微调必须用Trainer——因为:

  • Gradient Accumulation:Trainer支持args.gradient_accumulation_steps=4,用小batch模拟大batch;
  • Mixed Precision Training:自动插入GradScaler处理float16梯度;
  • Checkpointing:每500步自动保存,断电也不丢进度;
  • Metrics Logging:集成TensorBoard/Weights & Biases。

但Trainer也有坑:

  • 默认args.fp16=False,即使你用model.half(),梯度仍是float32,显存不省;
  • args.evaluation_strategy="steps"时,eval_steps=500意味着每500步验证一次,但若训练集只有1000样本,可能验证10次才过一遍数据;
  • 最致命的是:Trainer的compute_metrics函数接收EvalPrediction对象,而predictions是logits,必须手动softmax,否则F1分数算错。

正确写法:

def compute_metrics(eval_pred): predictions, labels = eval_pred preds = np.argmax(predictions, axis=1) # ← 这里必须argmax,不能直接用logits return {"accuracy": accuracy_score(labels, preds)}

经验:首次微调时,永远先用Trainer跑1个epoch,检查loss是否从inf降到2.x,再调超参。我见过太多人直接设num_train_epochs=10,结果第1步loss就是nan,浪费3小时GPU时间。

5. 预训练模型的四大能力边界:别让BERT干GPT的活

“预训练大模型”不是万能膏药。强行让一个模型做它没被设计的任务,就像让挖掘机开飞机——理论上都能动,但结果必然灾难。

5.1 架构决定能力:Encoder、Decoder、Encoder-Decoder的不可互换性

模型类型代表模型输入输出典型任务能力边界
Encoder-onlyBERT, RoBERTa文本→向量分类、NER、相似度❌ 不能生成文本(无因果掩码)
Decoder-onlyGPT-2, Llama文本→文本生成、补全、对话❌ 不能做序列标注(无双向上下文)
Encoder-DecoderT5, BART文本→文本翻译、摘要、问答✅ 可双向建模,但效率低于专用架构

真实案例:

  • 用BERT做摘要?必须加额外的decoder层,且效果不如BART;
  • 用GPT-2做情感分类?需在输入末尾加[LABEL]token,再预测下一个token,但准确率比BERT低5%;
  • 用T5做命名实体识别?可行,但参数量是BERT的3倍,推理延迟高40%。

关键判断法则:看模型config.architectures字段。如果是["BertModel"],就别想生成;如果是["GPT2LMHeadModel"],就别想分类。

5.2 任务头(Head)的硬约束:为什么AutoModelForXXX不能乱用?

AutoModelForSequenceClassification不是“通用分类器”,而是BERT + 分类头的组合:

  • 分类头结构:nn.Linear(config.hidden_size, num_labels);
  • 输入:取last_hidden_state[:, 0, :]([CLS] token的向量);
  • 输出:logits。

如果你用它做回归任务(预测房价),会报错:

model = AutoModelForSequenceClassification.from_pretrained("bert-base-chinese", num_labels=1) # 但SequenceClassification的loss是CrossEntropyLoss,要求labels是long类型 # 而回归需要MSELoss,labels是float类型

正确做法:

from transformers import AutoModel model = AutoModel.from_pretrained("bert-base-chinese") # 自定义head class RegressionHead(nn.Module): def __init__(self, hidden_size): super().__init__() self.dense = nn.Linear(hidden_size, 1) def forward(self, x): return self.dense(x[:, 0, :]).squeeze(-1) # [batch, 1] → [batch] regression_head = RegressionHead(model.config.hidden_size)

5.3 长文本处理的物理极限:Position Embedding与RoPE的代际差异

BERT的position embedding最大长度是512,超过会截断。但GPT-2是1024,LLaMA是2048,Qwen是32768——这不是版本升级,而是位置编码范式的革命。

  • Absolute Position Embedding(BERT):

    • 预先训练好的512维向量,位置i对应pe[i];
    • 超出范围直接报错或截断。
  • Rotary Position Embedding(RoPE,LLaMA/Qwen):

    • 通过旋转矩阵将位置信息注入query/key向量;
    • 理论上支持无限长度,但实际受KV Cache显存限制。

实测对比(A100 40GB):

模型最大长度2048长度显存推理延迟
BERT-base512不支持—
GPT-2102418.2 GB124ms
LLaMA-7B204822.1 GB156ms
Qwen-7B3276824.3 GB168ms

注意:RoPE模型虽支持长文本,但tokenizer的max_length参数仍需手动设大,否则tokenizer.encode()会截断。我曾因忘记改max_length=4096,导致Qwen只看到文章前512字。

6. 生产环境避坑清单:从开发机到服务器的七道关卡

本地跑通≠线上可用。我把过去三年部署27个NLP服务的经验,浓缩成七道必须通关的检查项。

6.1 关卡1:Tokenizer的确定性——为什么同一文本两次encode结果不同?

tokenizer.encode("hello")有时返回[101, 7592, 102],有时返回[101, 7592, 102, 0, 0]?
根源在padding策略:

  • padding=True:动态填充到batch中最长序列;
  • padding="max_length":固定填充到指定长度;
  • padding="longest":同True。

生产环境唯一安全策略:

# ✅ 永远指定max_length inputs = tokenizer( texts, padding="max_length", truncation=True, max_length=512, return_tensors="pt" ) # ❌ 禁止用padding=True(batch内长度不一致,DataLoader会报错)

6.2 关卡2:Batch Size的显存幻觉——为什么batch=16显存爆了,batch=8却正常?

显存占用 ≠ batch_size × 单样本显存。
真实公式:

显存 ≈ (batch_size × seq_len × hidden_size × 4) × 3(前向+反向+优化器状态)

但还有隐藏项:

  • KV Cache:Decoder模型(GPT类)会缓存key/value,显存随seq_len²增长;
  • Gradient Checkpointing:用时间换空间,显存降40%,但速度慢25%;
  • CUDA Graphs:PyTorch 2.0+特性,固化计算图,提速15%,但需固定shape。

实测建议:

  • 首次部署,用batch_size=1测baseline显存;
  • 再用batch_size=2,4,8测线性度;
  • 若非线性增长(如batch=4显存是batch=2的2.8倍),说明有KV Cache或梯度累积干扰。

6.3 关卡3:模型权重的完整性校验——如何10秒发现下载损坏?

Hugging Face模型文件用SHA256校验,但from_pretrained()不校验。手动校验:

import hashlib def verify_model_files(model_path): for file in ["pytorch_model.bin", "config.json", "tokenizer.json"]: path = f"{model_path}/{file}" if os.path.exists(path): with open(path, "rb") as f: sha256 = hashlib.sha256(f.read()).hexdigest() print(f"{file}: {sha256[:8]}...") verify_model_files("./bert-base-chinese")

官方SHA256在https://huggingface.co/bert-base-chinese/commit/main的refs/convert/resolve/main里可查。

6.4 关卡4:Docker镜像的Python版本陷阱——为什么conda环境OK,Docker里报错?

transformers依赖tokenizers>=0.13.0,而tokenizers要求rust>=1.60。
但Ubuntu 20.04默认rustc是1.54,Docker镜像若用ubuntu:20.04,pip install transformers会静默降级tokenizers到0.12.1,导致tokenizer.encode()返回空列表。

解决方案:

# ✅ 正确Dockerfile FROM ubuntu:22.04 RUN apt-get update && apt-get install -y curl RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="/root/.cargo/bin:$PATH" RUN pip install transformers torch

6.5 关卡5:GPU驱动与CUDA版本的隐性冲突——为什么A100上跑不动,V100上却OK?

transformers4.30+要求CUDA 11.8+,但NVIDIA驱动版本必须≥525(A100)或≥470(V100)。
检查命令:

nvidia-smi # 看驱动版本 nvcc --version # 看CUDA版本 python -c "import torch; print(torch.version.cuda)" # 看PyTorch编译的CUDA版本

三者必须兼容:

  • 驱动版本 ≥ CUDA要求的最低驱动;
  • PyTorch CUDA版本 ≤ 系统CUDA版本;
  • transformers版本 ≤ PyTorch支持的最大版本。

6.6 关卡6:多进程推理的句柄泄漏——为什么服务跑24小时后OOM?

transformers的pipeline()默认用torch.multiprocessing,但Linux默认ulimit -n是1024,每个worker进程打开的文件句柄(包括模型权重文件)会耗尽。

修复方案:

# 启动服务前 import resource resource.setrlimit(resource.RLIMIT_NOFILE, (65536, 65536)) # 或在Docker run时 docker run --ulimit nofile=65536:65536 ...

6.7 关卡7:模型热更新的原子性——如何零停机切换新模型?

不能直接rm -rf old_model && cp -r new_model,因为from_pretrained()会缓存路径。
正确流程:

  1. 将新模型放在/models/bert-v2/;
  2. 更新符号链接:ln -sf /models/bert-v2 /models/current;
  3. 在代码中用AutoModel.from_pretrained("/models/current");
  4. 重启worker进程(非kill -9,用SIGTERM优雅退出)。

最后提醒:所有生产环境变量(如TRANSFORMERS_OFFLINE=1)必须在启动脚本里显式声明,避免依赖网络下载。

我在实际使用中发现,最可靠的部署方式,永远是“先本地复现,再容器化,最后灰度发布”。
曾经有个项目,本地用transformers==4.28.1跑通,上线用4.30.2,结果Trainer的save_steps行为变了——旧版每100步保存,新版每100步保存+评估。没做灰度,直接全量,导致线上服务每100步就写一次磁盘,IO打满。
后来我们定了铁律:任何transformers版本升级,必须跑完全部单元测试+压力测试,且灰度比例从1%开始,观察2小时metrics无异常,再逐步放大。
这听起来很重,但比起半夜被报警电话叫醒,这点成本值得。
如果你也打算把模型放进生产系统,不妨现在就打开终端,执行一遍pip list | grep transformers,看看版本号——它可能已经悄悄变了。

返回列表