Transformers 4.37 中文文档(十二)(2)https://developer.aliyun.com/article/1564913
字节对编码(BPE)
字节对编码(BPE)是在Neural Machine Translation of Rare Words with Subword Units (Sennrich et al., 2015)中引入的。BPE 依赖于一个预分词器,将训练数据分割成单词。预分词可以简单到空格分词,例如 GPT-2,RoBERTa。更高级的预分词包括基于规则的分词,例如 XLM,FlauBERT 使用 Moses 用于大多数语言,或者 GPT 使用 Spacy 和 ftfy,来计算训练语料库中每个单词的频率。
在预分词之后,已创建了一组唯一的单词,并确定了每个单词在训练数据中出现的频率。接下来,BPE 创建一个基本词汇,其中包含所有出现在唯一单词集合中的符号,并学习合并规则,以从基本词汇的两个符号形成一个新符号。它会一直这样做,直到词汇表达到所需的词汇量。请注意,所需的词汇量是在训练分词器之前定义的一个超参数。
举个例子,假设在预分词之后,已确定了以下包含频率的单词集合:
("hug", 10), ("pug", 5), ("pun", 12), ("bun", 4), ("hugs", 5)
因此,基本词汇是["b", "g", "h", "n", "p", "s", "u"]
。将所有单词分割为基本词汇的符号,我们得到:
("h" "u" "g", 10), ("p" "u" "g", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "u" "g" "s", 5
BPE 然后计算每对可能符号的频率,并选择出现最频繁的符号对。在上面的例子中,"h"
后跟"u"
出现了10 + 5 = 15次(在 10 次"hug"
出现中的 10 次,以及在 5 次"hugs"
出现中的 5 次)。然而,最频繁的符号对是"u"
后跟"g"
,总共出现了10 + 5 + 5 = 20次。因此,分词器学习的第一个合并规则是将所有跟在"u"
符号后面的"g"
符号组合在一起。接下来,"ug"
被添加到词汇表中。然后单词集合变为
("h" "ug", 10), ("p" "ug", 5), ("p" "u" "n", 12), ("b" "u" "n", 4), ("h" "ug" "s", 5)
BPE 然后识别下一个最常见的符号对。它是"u"
后跟"n"
,出现了 16 次。"u"
、"n"
被合并为"un"
并添加到词汇表中。下一个最频繁的符号对是"h"
后跟"ug"
,出现了 15 次。再次合并这对,并且"hug"
可以被添加到词汇表中。
在这个阶段,词汇表是["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"]
,我们的唯一单词集合表示为
("hug", 10), ("p" "ug", 5), ("p" "un", 12), ("b" "un", 4), ("hug" "s", 5)
假设字节对编码训练在这一点停止,那么学习到的合并规则将被应用于新单词(只要这些新单词不包含基本词汇中没有的符号)。例如,单词"bug"
将被分词为["b", "ug"]
,但"mug"
将被分词为["", "ug"]
,因为符号"m"
不在基本词汇中。通常情况下,像"m"
这样的单个字母不会被""
符号替换,因为训练数据通常至少包含每个字母的一个出现,但对于非常特殊的字符,比如表情符号,可能会发生这种情况。
如前所述,词汇量,即基本词汇量+合并次数,是一个需要选择的超参数。例如 GPT 的词汇量为 40,478,因为它们有 478 个基本字符,并选择在 40,000 次合并后停止训练。
字节级 BPE
如果将所有 Unicode 字符视为基本字符,那么包含所有可能基本字符的基本词汇可能会非常庞大。为了获得更好的基本词汇,GPT-2 使用字节作为基本词汇,这是一个巧妙的技巧,可以强制基本词汇的大小为 256,同时确保每个基本字符都包含在词汇中。通过一些额外的规则来处理标点符号,GPT2 的分词器可以对每个文本进行分词,而无需使用符号。GPT-2 的词汇量为 50,257,对应于 256 个字节基本标记、一个特殊的文本结束标记和通过 50,000 次合并学习的符号。
WordPiece
WordPiece 是用于 BERT、DistilBERT 和 Electra 的子词分词算法。该算法在Japanese and Korean Voice Search (Schuster et al., 2012)中概述,与 BPE 非常相似。WordPiece 首先将词汇表初始化为包含训练数据中的每个字符,并逐渐学习一定数量的合并规则。与 BPE 不同,WordPiece 不选择最频繁的符号对,而是选择一旦添加到词汇表中就最大化训练数据的可能性的符号对。
那么这到底意味着什么呢?参考前面的例子,最大化训练数据的可能性等同于找到符号对,其概率除以其第一个符号后跟第二个符号的概率在所有符号对中最大。例如,"u"
后跟"g"
只有在"ug"
的概率除以"u"
、"g"
的概率大于任何其他符号对时才会被合并。直觉上,WordPiece 与 BPE 略有不同,因为它评估合并两个符号会损失什么,以确保值得。
Unigram
Unigram 是一种子词分词算法,由Kudo, 2018引入。与 BPE 或 WordPiece 相比,Unigram 将其基本词汇初始化为大量符号,并逐渐修剪每个符号以获得较小的词汇表。基本词汇表可以对应于所有预分词的单词和最常见的子字符串。Unigram 不直接用于 transformers 中的任何模型,但与 SentencePiece 一起使用。
在每个训练步骤中,Unigram 算法根据当前词汇表和 unigram 语言模型定义了一个损失(通常定义为对数似然)。然后,对于词汇表中的每个符号,该算法计算如果将该符号从词汇表中移除会导致整体损失增加多少。Unigram 然后删除损失增加最低的 p(通常为 10%或 20%)百分比的符号,即那些对训练数据整体损失影响最小的符号。这个过程重复进行,直到词汇表达到所需大小。Unigram 算法始终保留基本字符,以便任何单词都可以被分词。
由于 Unigram 不基于合并规则(与 BPE 和 WordPiece 相反),该算法在训练后有几种分词新文本的方式。例如,如果经过训练的 Unigram 分词器展示以下词汇表:
["b", "g", "h", "n", "p", "s", "u", "ug", "un", "hug"],
"hugs"
可以被分词为["hug", "s"]
、["h", "ug", "s"]
或["h", "u", "g", "s"]
。那么应该选择哪一个?Unigram 在保存词汇的同时还保存了训练语料库中每个标记的概率,以便在训练后计算每种可能的分词的概率。该算法实际上只选择最有可能的分词,但也提供了根据它们的概率对可能的分词进行抽样的可能性。
这些概率由标记器训练时定义的损失来确定。假设训练数据由单词 x1,…,xN 组成,并且对于单词 xi 的所有可能标记化集合定义为 S(xi),则总损失定义为 L=−i=1∑Nlogx∈S(xi)∑p(x)。
SentencePiece
到目前为止,所有描述的标记化算法都有同样的问题:假设输入文本使用空格来分隔单词。然而,并非所有语言都使用空格来分隔单词。一个可能的解决方案是使用特定语言的预分词器,例如 XLM 使用特定的中文、日文和泰文预分词器。为了更普遍地解决这个问题,SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (Kudo et al., 2018) 将输入视为原始输入流,因此包括空格在要使用的字符集中。然后使用 BPE 或 unigram 算法构建适当的词汇表。
XLNetTokenizer 例如使用了 SentencePiece,这也是为什么在前面的例子中包含了 "▁"
字符在词汇表中。使用 SentencePiece 进行解码非常容易,因为所有标记只需连接在一起,而 "▁"
被替换为一个空格。
库中所有使用 SentencePiece 的变压器模型都与 unigram 结合使用。使用 SentencePiece 的模型示例包括 ALBERT, XLNet, Marian 和 T5。
注意机制
大多数 transformer 模型在注意力矩阵是方形的意义上使用全注意力。当处理长文本时,这可能是一个巨大的计算瓶颈。Longformer 和 reformer 是试图更高效并使用注意力矩阵的稀疏版本来加速训练的模型。
LSH 注意力
Reformer 使用 LSH 注意力。在 softmax(QK^t) 中,只有矩阵 QK^t 中最大的元素(在 softmax 维度上)才会提供有用的贡献。因此,对于 Q 中的每个查询 q,我们只考虑与 q 接近的 K 中的键 k。使用哈希函数来确定 q 和 k 是否接近。注意力掩码被修改为掩盖当前标记(除了第一个位置),因为它会给出一个相等的查询和键(因此非常相似)。由于哈希可能有点随机,实践中使用了几个哈希函数(由 n_rounds 参数确定),然后对它们进行平均。
本地注意力
Longformer 使用本地注意力:通常,局部上下文(例如,左右两个标记是什么?)足以为给定标记采取行动。此外,通过堆叠具有小窗口的注意力层,最后一层将具有超出窗口中标记的感受野,使它们能够构建整个句子的表示。
还有一些预选的输入标记也被给予全局注意力:对于这几个标记,注意力矩阵可以访问所有标记,这个过程是对称的:所有其他标记都可以访问这些特定标记(除了它们本地窗口中的标记)。这在论文的图 2d 中显示,下面是一个示例注意力掩码:
使用这些具有更少参数的注意力矩阵使模型能够具有更大的序列长度。
其他技巧
轴向位置编码
Reformer 使用轴向位置编码:在传统的 Transformer 模型中,位置编码 E 是一个大小为lll 乘以ddd 的矩阵,其中lll 是序列长度,ddd 是隐藏状态的维度。如果您有非常长的文本,这个矩阵可能会非常庞大,在 GPU 上占用太多空间。为了缓解这个问题,轴向位置编码包括将这个大矩阵 E 分解为两个较小的矩阵 E1 和 E2,其维度分别为l1×d1l_{1} \times d_{1}l1×d1和l2×d2l_{2} \times d_{2}l2×d2,使得l1×l2=ll_{1} \times l_{2} = ll1×l2=l 和d1+d2d_{1} + d_{2} = dd1+d2=d(长度的乘积使得结果变得更小)。在矩阵 E 中,时间步jjj 的嵌入是通过将 E1 中时间步j%l1j % l1j%l1 的嵌入和 E2 中时间步j//l1j // l1j//l1 的嵌入进行连接获得的。
填充和截断
原文链接:
huggingface.co/docs/transformers/v4.37.2/en/pad_truncation
批量输入通常具有不同的长度,因此无法转换为固定大小的张量。填充和截断是处理此问题的策略,以从不同长度的批次创建矩形张量。填充添加一个特殊的填充标记,以确保较短的序列将具有与批次中最长序列或模型接受的最大长度相同的长度。截断则是截断长序列。
在大多数情况下,将批次填充到最长序列的长度,并截断到模型可以接受的最大长度通常效果很好。但是,如果需要,API 支持更多策略。您需要的三个参数是:padding
,truncation
和max_length
。
padding
参数控制填充。它可以是布尔值或字符串:
True
或'longest'
:填充到批次中最长的序列(如果只提供单个序列,则不会应用填充)。'max_length'
:通过max_length
参数指定的长度填充,或者如果没有提供max_length
,则填充到模型接受的最大长度(max_length=None
)。如果只提供单个序列,仍将应用填充。False
或'do_not_pad'
:不会应用填充。这是默认行为。
truncation
参数控制截断。它可以是布尔值或字符串:
True
或'longest_first'
:通过max_length
参数指定的最大长度截断,或者如果没有提供max_length
,则截断到模型接受的最大长度(max_length=None
)。这将逐标记截断,从一对中最长的序列中删除一个标记,直到达到适当的长度。'only_second'
:通过max_length
参数指定的最大长度截断,或者如果没有提供max_length
,则截断到模型接受的最大长度(max_length=None
)。如果提供了一对序列(或一批序列对),则只会截断第二句。'only_first'
: 通过max_length
参数指定的最大长度截断,或者如果没有提供max_length
,则截断到模型接受的最大长度(max_length=None
)。如果提供了一对序列(或一批序列对),则只会截断第一句。False
或'do_not_truncate'
:不会应用截断。这是默认行为。
max_length
参数控制填充和截断的长度。它可以是整数或None
,在这种情况下,它将默认为模型可以接受的最大长度。如果模型没有特定的最大输入长度,截断或填充到max_length
将被禁用。
以下表格总结了设置填充和截断的推荐方式。如果在以下示例中使用输入序列对,可以将truncation=True
替换为在['only_first', 'only_second', 'longest_first']
中选择的STRATEGY
,即truncation='only_second'
或truncation='longest_first'
以控制如前所述截断一对中的两个序列。
截断 | 填充 | 指令 |
无截断 | 无填充 | tokenizer(batch_sentences) |
填充到批次中的最大序列 | tokenizer(batch_sentences, padding=True) 或 |
|
tokenizer(batch_sentences, padding='longest') |
||
填充到最大模型输入长度 | tokenizer(batch_sentences, padding='max_length') |
|
填充到特定长度 | tokenizer(batch_sentences, padding='max_length', max_length=42) |
|
填充到值的倍数 | tokenizer(batch_sentences, padding=True, pad_to_multiple_of=8) |
|
截断到最大模型输入长度 | 无填充 | tokenizer(batch_sentences, truncation=True) 或 |
tokenizer(batch_sentences, truncation=STRATEGY) |
||
填充到批次中的最大序列长度 | tokenizer(batch_sentences, padding=True, truncation=True) 或 |
|
tokenizer(batch_sentences, padding=True, truncation=STRATEGY) |
||
填充到最大模型输入长度 | tokenizer(batch_sentences, padding='max_length', truncation=True) 或 |
|
tokenizer(batch_sentences, padding='max_length', truncation=STRATEGY) |
||
填充到特定长度 | 不可能 | |
截断到特定长度 | 不填充 | tokenizer(batch_sentences, truncation=True, max_length=42) 或 |
tokenizer(batch_sentences, truncation=STRATEGY, max_length=42) |
||
填充到批次中的最大序列长度 | tokenizer(batch_sentences, padding=True, truncation=True, max_length=42) 或 |
|
tokenizer(batch_sentences, padding=True, truncation=STRATEGY, max_length=42) |
||
填充到最大模型输入长度 | 不可能 | |
填充到特定长度 | tokenizer(batch_sentences, padding='max_length', truncation=True, max_length=42) 或 |
|
tokenizer(batch_sentences, padding='max_length', truncation=STRATEGY, max_length=42) |
BERTology
有一个不断增长的研究领域,关注调查大规模变压器(如 BERT)的内部工作(有些人称之为“BERTology”)。 这个领域的一些很好的例子是:
- BERT 重新发现了经典的 NLP 流程 作者:Ian Tenney,Dipanjan Das,Ellie Pavlick:
arxiv.org/abs/1905.05950
- 十六个头真的比一个好吗? 作者:Paul Michel,Omer Levy,Graham Neubig:
arxiv.org/abs/1905.10650
- BERT 看什么? 作者:Kevin Clark,Urvashi Khandelwal,Omer Levy,Christopher D. Manning:
arxiv.org/abs/1906.04341
- CAT 探测:一种基于度量的方法,解释预训练模型如何关注代码结构:
arxiv.org/abs/2210.04633
为了帮助这个新领域发展,我们在 BERT/GPT/GPT-2 模型中添加了一些额外功能,以帮助人们访问内部表示,主要是从 Paul Michel 的伟大工作中改编的(arxiv.org/abs/1905.10650
):
- 访问 BERT/GPT/GPT-2 的所有隐藏状态,
- 访问 BERT/GPT/GPT-2 每个头的所有注意权重,
- 检索头输出值和梯度,以便计算头重要性分数并修剪头,如
arxiv.org/abs/1905.10650
中所解释的。
为了帮助您理解和使用这些功能,我们添加了一个特定的示例脚本:bertology.py,同时提取在 GLUE 上预训练的模型的信息并修剪。
固定长度模型的困惑度
困惑度(PPL)是评估语言模型最常见的指标之一。在深入讨论之前,我们应该注意,该指标特别适用于传统语言模型(有时称为自回归或因果语言模型),对于像 BERT 这样的掩码语言模型,该指标并不明确定义(请参阅模型摘要)。
困惑度被定义为序列的指数化平均负对数似然。如果我们有一个标记化的序列X=(x0,x1,…,xt)X = (x_0, x_1, \dots, x_t)X=(x0,x1,…,xt),那么XXX 的困惑度为,PPL(X)=exp{−1t∑itlogpθ(xi∣x
其中logpθ(xi∣x
这也等同于数据和模型预测之间交叉熵的指数。关于困惑度及其与每字符比特(BPC)和数据压缩的关系的更多直觉,请查看The Gradient上的这篇精彩博客文章。
使用固定长度模型计算 PPL
如果我们不受模型上下文大小的限制,我们将通过自回归地分解序列并在每一步上都对整个先前子序列进行条件评估模型的困惑度,如下所示。
然而,在使用近似模型时,通常会对模型可以处理的标记数量有限制。例如,GPT-2 的最大版本具有固定长度的 1024 个标记,因此当 t 大于 1024 时,我们无法直接计算 pθ(xt∣x
相反,序列通常被分成与模型最大输入大小相等的子序列。如果模型的最大输入大小为 k,那么我们通过仅在之前的 k-1 个标记上进行条件化来近似标记 xt的可能性,而不是整个上下文。在评估序列的模型困惑度时,一种诱人但次优的方法是将序列分成不相交的块,并独立地将每个段的分解对数似然相加。
不利用完全可用上下文的次优 PPL
这种方法计算快速,因为每个段的困惑度可以在一个前向传递中计算,但作为完全因子化困惑度的一个很差的近似,并且通常会产生更高(更差)的 PPL,因为模型在大多数预测步骤中将具有更少的上下文。
相反,应该使用滑动窗口策略评估固定长度模型的 PPL。这涉及反复滑动上下文窗口,以便模型在进行每个预测时具有更多上下文。
利用所有可用上下文的滑动窗口 PPL
这是对序列概率的真实分解的更接近近似,并且通常会产生更有利的分数。缺点是它需要为语料库中的每个标记进行单独的前向传递。一个很好的实际折衷方案是使用跨度滑动窗口,通过更大的跨度移动上下文,而不是每次滑动一个标记。这样可以使计算速度更快,同时仍然使模型在每一步中具有更大的上下文来进行预测。
示例:在🤗 Transformers 中使用 GPT-2 计算困惑度
让我们用 GPT-2 演示这个过程。
from transformers import GPT2LMHeadModel, GPT2TokenizerFast device = "cuda" model_id = "gpt2-large" model = GPT2LMHeadModel.from_pretrained(model_id).to(device) tokenizer = GPT2TokenizerFast.from_pretrained(model_id)
我们将加载 WikiText-2 数据集,并使用几种不同的滑动窗口策略评估困惑度。于这个数据集很小,我们只需对整个数据集进行一次前向传递,因此可以将整个数据集加载和编码到内存中。
from datasets import load_dataset test = load_dataset("wikitext", "wikitext-2-raw-v1", split="test") encodings = tokenizer("\n\n".join(test["text"]), return_tensors="pt")
使用🤗 Transformers,我们可以简单地将input_ids
作为labels
传递给我们的模型,每个标记的平均负对数似然将作为损失返回。然而,使用我们的滑动窗口方法,在每次迭代中传递给模型的标记存在重叠。我们不希望将我们只将其视为上下文的标记的对数似然包括在我们的损失中,因此我们可以将这些目标设置为-100
,以便忽略它们。以下是我们如何使用步幅为512
的示例。这意味着在计算任何一个标记的条件概率时,模型将至少有 512 个标记的上下文(前提是有 512 个先前的标记可用于条件)。
import torch from tqdm import tqdm max_length = model.config.n_positions stride = 512 seq_len = encodings.input_ids.size(1) nlls = [] prev_end_loc = 0 for begin_loc in tqdm(range(0, seq_len, stride)): end_loc = min(begin_loc + max_length, seq_len) trg_len = end_loc - prev_end_loc # may be different from stride on last loop input_ids = encodings.input_ids[:, begin_loc:end_loc].to(device) target_ids = input_ids.clone() target_ids[:, :-trg_len] = -100 with torch.no_grad(): outputs = model(input_ids, labels=target_ids) # loss is calculated using CrossEntropyLoss which averages over valid labels # N.B. the model only calculates loss over trg_len - 1 labels, because it internally shifts the labels # to the left by 1. neg_log_likelihood = outputs.loss nlls.append(neg_log_likelihood) prev_end_loc = end_loc if end_loc == seq_len: break ppl = torch.exp(torch.stack(nlls).mean())
将步长设置为最大输入长度时运行此操作等同于我们上面讨论的次优、非滑动窗口策略。步长越小,模型在进行每次预测时获得的上下文就越多,通常报告的困惑度也会更好。
当我们使用 stride = 1024
,即没有重叠时,得到的困惑度为 19.44
,与 GPT-2 论文中报告的 19.93
差不多。通过使用 stride = 512
,从而采用我们的滑动窗口策略,这个值降至 16.45
。这不仅是一个更有利的分数,而且计算方式更接近于序列可能性的真实自回归分解。
为 Web 服务器使用管道
原文:
huggingface.co/docs/transformers/v4.37.2/en/pipeline_webserver
创建推断引擎是一个复杂的主题,“最佳”解决方案很可能取决于您的问题空间。您是在 CPU 还是 GPU 上?您想要最低的延迟、最高的吞吐量、对许多模型的支持,还是只是高度优化一个特定模型?解决这个问题的方法有很多,因此我们将提供一个很好的默认值来开始,这可能不一定是最优的解决方案。
关键的理解是,我们可以使用一个迭代器,就像你在数据集上使用的一样,因为 Web 服务器基本上是一个等待请求并按顺序处理它们的系统。
通常,Web 服务器是多路复用的(多线程、异步等),以处理各种请求。另一方面,管道(以及大多数底层模型)并不真正适合并行处理;它们占用大量 RAM,因此最好在运行时为它们提供所有可用的资源,或者这是一个计算密集型的工作。
我们将通过让 Web 服务器处理接收和发送请求的轻负载,并让单个线程处理实际工作来解决这个问题。这个示例将使用starlette
。实际的框架并不是很重要,但如果您使用另一个框架来实现相同的效果,可能需要调整或更改代码。
创建server.py
:
from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.routing import Route from transformers import pipeline import asyncio async def homepage(request): payload = await request.body() string = payload.decode("utf-8") response_q = asyncio.Queue() await request.app.model_queue.put((string, response_q)) output = await response_q.get() return JSONResponse(output) async def server_loop(q): pipe = pipeline(model="bert-base-uncased") while True: (string, response_q) = await q.get() out = pipe(string) await response_q.put(out) app = Starlette( routes=[ Route("/", homepage, methods=["POST"]), ], ) @app.on_event("startup") async def startup_event(): q = asyncio.Queue() app.model_queue = q asyncio.create_task(server_loop(q))
现在您可以启动它:
uvicorn server:app
你可以查询它:
curl -X POST -d "test [MASK]" http://localhost:8000/ #[{"score":0.7742936015129089,"token":1012,"token_str":".","sequence":"test."},...]
现在,您已经了解如何创建 Web 服务器了!
真正重要的是,我们只一次加载模型,因此在 Web 服务器上没有模型的副本。这样,就不会使用不必要的 RAM。然后,排队机制允许您执行一些花哨的操作,比如可能在推断之前累积一些项目以使用动态批处理:
下面的代码示例故意以伪代码形式编写,以提高可读性。在未检查是否对您的系统资源有意义的情况下,请勿运行此代码!
(string, rq) = await q.get() strings = [] queues = [] while True: try: (string, rq) = await asyncio.wait_for(q.get(), timeout=0.001) # 1ms except asyncio.exceptions.TimeoutError: break strings.append(string) queues.append(rq) strings outs = pipe(strings, batch_size=len(strings)) for rq, out in zip(queues, outs): await rq.put(out)
再次强调,建议的代码是为了可读性而优化的,而不是为了成为最佳代码。首先,没有批处理大小限制,这通常不是一个好主意。其次,超时在每次队列获取时重置,这意味着您可能需要等待比 1 毫秒更长的时间才能运行推断(延迟第一个请求)。
最好设置一个单独的 1 毫秒截止时间。
即使队列为空,这将始终等待 1 毫秒,这可能不是最好的,因为如果队列中没有东西,您可能希望开始进行推断。但如果批处理对您的用例非常关键,那么这可能是有意义的。再次强调,没有一个最佳解决方案。
你可能想考虑的几件事
错误检查
在生产中可能会出现很多问题:内存不足、空间不足、加载模型可能失败、查询可能错误、查询可能正确但由于模型配置错误而无法运行,等等。
通常,如果服务器将错误输出给用户,那么添加许多try..except
语句来显示这些错误是一个好主意。但请记住,根据您的安全上下文,公开所有这些错误也可能是一个安全风险。
断路处理
当 Web 服务器过载时,通常最好进行断路处理。这意味着它们在过载时返回适当的错误,而不是无限期地等待查询。在等待超长时间后返回 503 错误,或者在很长时间后返回 504 错误。
在建议的代码中实现这个相对容易,因为有一个单一的队列。查看队列大小是在 Web 服务器在负载下失败之前开始返回错误的基本方法。
阻塞主线程
目前 PyTorch 不支持异步操作,计算时会阻塞主线程。这意味着最好让 PyTorch 在自己的线程/进程上运行。这里没有这样做是因为代码更加复杂(主要是因为线程、异步和队列不太兼容)。但最终它做的事情是一样的。
如果单个项目的推断时间很长(> 1 秒),这将是很重要的,因为在这种情况下,在推断期间每个查询都必须等待 1 秒才能收到错误。
动态批处理
一般来说,批处理不一定比逐个传递项目更好(有关更多信息,请参阅批处理详细信息)。但在正确的环境中使用时,它可能非常有效。在 API 中,默认情况下没有动态批处理(太容易导致减速)。但对于 BLOOM 推断 - 这是一个非常庞大的模型 - 动态批处理是必不可少的,以为每个人提供良好的体验。
Transformers 4.37 中文文档(十二)(4)https://developer.aliyun.com/article/1564915