Qiming Bao · Blog · 2026 年 11 月 13 日 · English / 中文

不用预训练,在 16GB 显卡上用 LayoutLMv3 处理长文档

LayoutLMv3 最多只能读 512 个文本 token。收据这类短文档没问题,但一页账单或订单常有几十行明细,往往超过 1000 个 token,第 512 个 token 之后的内容模型根本看不到。我想要一个能读完整页的版本,并且满足三个条件:不额外预训练;能在 16GB 的 V100 上推理;显存和速度接近 512 的模型。本文介绍最后行之有效的做法,代码在我的 layoutlmv3-pretraining 仓库。

截断到底丢了多少?

常用的 FUNSD 太短,看不出差别:50 张测试表单里只有 4 张超过 512 个 token。我改用 Google 的富版面文档基准 VRDU 中的 Ad-buy Forms:电视广告订单,每页是一张行项目表格,标注了频道、节目、日期、金额等 14 个字段。我把每一页转成 token 分类任务(字段的 BIO 标签)。

VRDU Ad-buy 测试集(774 页)数值
页面长度:中位数 / 90 分位 / 最长829 / 1,471 / 5,105 个 token
超过 512 个 token 的页面77%
截断到 512 / 1024 / 2048 时丢失的标注词40.8% / 5.5% / 0%

只读 512 个 token 时,模型要标注的词有四成根本看不到。

不预训练,读更长的页面

LayoutLMv3 读不了 512 以上有两个原因:它只学过 512 个位置编码;注意力的显存随长度的平方增长。

位置。我把 512 个预训练的一维位置编码重复拼接到更长(Longformer 用过的复制初始化),然后照常在任务上微调,没有训练任何别的东西。这个做法效果不错,是因为 LayoutLMv3 还有二维版面编码和相对位置偏置,它们不受长度影响。另一种做法是带重叠的 512 滑动窗口,再按词合并预测。

结果(LayoutLMv3-base,微调 2000 步,批大小 2,5 个随机种子,每个词都计分):

LayoutLMv3F1 × 1005 个种子的耗时
截断到 51268.31 ± 0.1321.5 分钟
512 滑动窗口,步长重叠 12890.97 ± 0.6525.8 分钟
位置扩展到 102489.87 ± 0.1326.8 分钟
位置扩展到 204892.52 ± 0.1131.9 分钟

2048 个 token 读完整页,比截断高 24 分,比滑动窗口高 1.6 分。可能的原因是:窗口只能看到表格的一部分,而读完整页时每一行都能看到表头和其他行。1024 在这里不够,三分之一的页面更长,尾部仍然被截掉。实验记录在 W&B 上(512、窗口、1024、2048)。

收敛速度。重复使用位置编码不会让微调变慢。每 250 步评估一次(3 个种子):512 和 2048 的训练损失下降速度相同,而 2048 在测试集上每个评估点都领先:第 250 步 36.3 对 19.0,第 500 步 74.1 对 53.7(已经超过截断最终的 68.3),第 1000 步 89.5 对 66.3。2048 在第 1500 步左右稳定在 92.7(W&B:512、2048)。这组实验训练时也用了查找表注意力。

ERNIE-Layout(通过社区的 PyTorch 移植版,用同样的位置复制)在同样的页面上结论一致:截断 70.87 ± 0.18,滑动窗口 92.78 ± 0.30,位置扩展到 2048 为 92.88 ± 0.17(W&B:512、窗口、2048)。读完整页同样比截断高 22 分,但对 ERNIE-Layout 来说和滑动窗口打平(+0.10,配对 t 检验 p = 0.28);而 LayoutLMv3 比窗口高 1.6 分,5 个种子全部更好(p = 0.007)。

速度靠动态补齐:每个批次只补齐到其中最长的文档,而不是补到 2048,所以短页面的开销和 512 的模型一样,只有真正长的页面才多花时间。2048 的微调时间是截断的 1.5 倍,而不是 4 倍。

显存究竟花在哪里

LayoutLMv3 会根据两个 token 的一维距离,以及它们在页面上的水平和垂直距离,给每个注意力分数加上可学习的偏置。transformers 的实现是先构建 n × n 的距离矩阵,用 fp32 计算对数分桶,再从偏置表里取值。我本以为 n × n 的偏置本身是问题,其实不是:2048 个 token 加 197 个图块时,fp16 的偏置只有约 121 MB,分桶计算的 fp32 临时数据要大好几倍。

分桶只取决于距离,而距离只有几千种可能。所以我按距离把每张表预先算好,直接用 fp16 查出合并后的偏置,12 层共用一份。结果是精确的,和 transformers 的注意力输出一致。注意力本身用 PyTorch 显存高效的 scaled_dot_product_attention 内核计算。

推理,一份文档,fp16,总显存(含权重)/ 时间:

文本 token 数512204840968192
transformers 原始注意力0.81 GB2.06 GB5.77 GB-
SDPA,transformers 的分桶0.69 GB / 36 ms1.49 GB / 91 ms4.15 GB / 251 ms显存不足
SDPA,查找表构建偏置0.69 GB / 35 ms0.80 GB / 66 ms1.57 GB / 151 ms4.58 GB / 484 ms

为了确认查找表在真实数据上不改变任何结果,我用一个在 VRDU 上以 2048 长度微调好的模型,分别用四种注意力实现评估 774 页测试集:之前的 SDPA、查找表、按查询分块的 SDPA 和 FlexAttention。四种实现的 F1 都是 92.96,每个标注 token 的预测都一样。在这次评估中(fp32,批大小 8,补齐到 2048),查找表也是最快的,显存 7.5 GB,原 SDPA 是 14.8 GB(W&B)。

2048 个 token 只需要 0.80 GB,512 时是 0.69 GB,而且比之前的 SDPA 版本更快。在 2048 长度、批大小 2 的条件下微调需要 7.05 GB,开启梯度检查点后为 3.99 GB;4096 加梯度检查点为 6.71 GB。

FlexAttention,以及为什么不推荐在 V100 上用

我最先试的是 FlexAttention,它在注意力内核里现场计算偏置,从不构建 n × n 的张量。在 A100 上效果很好:2048 个 token 用 0.74 GB,8192 个用 0.87 GB。它的第一版在 2048 个 token 时比 SDPA 慢约 4 倍(0.35 秒对 0.09 秒),因为每个分数都要单独分桶;改成按距离查表后就快了,上面的查找表方法正是从这里来的。

但 FlexAttention 用 Triton 编译内核,而新版 Triton 已不再使用 V100 等 Ampere 之前显卡的张量核心,所以在 V100 上不能指望 A100 上的速度。查找表方法用的是 PyTorch 自带的显存高效内核,会用到 V100 的张量核心,也不需要编译器。如果页面超过 8192 个 token,还可以按每块 512 个查询分块计算 SDPA:8192 个 token 只用 1.08 GB,代价是比查找表方法慢 2.7 到 4.4 倍(2048 到 8192 个 token)。

所有数字的前提:我是在 A100 上限制 15 GB 显存、只启用 V100 支持的注意力内核测得的,不是在真正的 V100 上。

哪些是新的,哪些不是

大部分组成部分都是已有的。Longformer(2020)证明了复制位置编码、不做额外预训练直接微调,在 WikiHop 上只比做了预训练的版本低约 0.6 分。LSG Attention(2022)用稀疏注意力把 BERT、RoBERTa、BART 转成长文本版本。LongEmbed(2024)不训练就扩展了 embedding 模型的长度。最近一篇论文(arXiv 2611.01182,2026 年 10 月)用 Triton 写了在 FlashAttention 式内核里精确计算相对位置注意力的方法。

我没有在别处找到的,是面向版面模型的这套组合:支持 LayoutLMv3 的一维和二维偏置;不预训练;完整注意力且精确(短页面的输出和原模型一样);只用 PyTorch 自带内核,能在 V100 上运行;并在真正的长页面上验证。2020 年需要稀疏注意力,是因为完整注意力放不进显存;有了显存高效的内核,2048 到 8192 个 token 的完整注意力在 16GB 显卡上也放得下。

接下来

代码、实验记录和上游贡献

编写代码、运行实验和撰写本文时,我使用了 Claude(Anthropic)辅助。所有结果和文章都经过我本人审阅。


包启明,现居新西兰奥克兰的 AI 研究者,研究方向为大模型推理与模型部署。博士毕业于奥克兰大学 Strong AI Lab,参与 PyTorch ONNX 导出器、ONNX Runtime、ONNX Runtime GenAI 和 Microsoft PyRIT 等开源项目的贡献。