不用预训练,在 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 个随机种子,每个词都计分):
| LayoutLMv3 | F1 × 100 | 5 个种子的耗时 |
|---|---|---|
| 截断到 512 | 68.31 ± 0.13 | 21.5 分钟 |
| 512 滑动窗口,步长重叠 128 | 90.97 ± 0.65 | 25.8 分钟 |
| 位置扩展到 1024 | 89.87 ± 0.13 | 26.8 分钟 |
| 位置扩展到 2048 | 92.52 ± 0.11 | 31.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 数 | 512 | 2048 | 4096 | 8192 |
|---|---|---|---|---|
| transformers 原始注意力 | 0.81 GB | 2.06 GB | 5.77 GB | - |
| SDPA,transformers 的分桶 | 0.69 GB / 36 ms | 1.49 GB / 91 ms | 4.15 GB / 251 ms | 显存不足 |
| SDPA,查找表构建偏置 | 0.69 GB / 35 ms | 0.80 GB / 66 ms | 1.57 GB / 151 ms | 4.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 显卡上也放得下。
接下来
- 在第二个长文档数据集上验证,并测试 4096 及更长的准确率:VRDU 里没有超过 2048 的标注词。
- 在 LSG 和 Longformer 的长文本基准上做对比,检验"复制位置 + 精确注意力"对纯文本 encoder 是否同样成立。
- 像 FlashBias(NeurIPS 2025)处理其他偏置那样,把版面偏置分解成低秩项拼进 query 和 key,彻底不构建 n × n 偏置。
- 在真正的 V100 上确认这些数字。
代码、实验记录和上游贡献
- 代码仓库:14H034160212/layoutlmv3-pretraining。README 的 "Long documents within a 16 GB GPU" 一节列出了所有表格及对应的 W&B 记录,原始结果在
results/long_docs/。 - 位置扩展和各种注意力实现:
src/layoutlmv3_pretraining/long_docs.py;微调:scripts/finetune_funsd.py;VRDU 数据转换:scripts/prepare_vrdu.py;显存和速度测试:scripts/bench_inference_memory.py、scripts/bench_train_memory.py。 - 在 VRDU 上的推荐设置:
python scripts/finetune_funsd.py --task vrdu --model microsoft/layoutlmv3-base --protocol strict --extend_positions --max_length 2048 --attention lut --dynamic_padding - transformers:#50688,为 LayoutLMv3 加入 SDPA 和查找表偏置(#35467)。
- ERNIE-Layout 移植版:#27(图像被归一化两次)、#30(tokenizer 在 transformers 4.45 及以上报错)、#31(相对位置偏置改用
index_select:训练每步快 3.8 倍,4096 推理也放得下)。
编写代码、运行实验和撰写本文时,我使用了 Claude(Anthropic)辅助。所有结果和文章都经过我本人审阅。
包启明,现居新西兰奥克兰的 AI 研究者,研究方向为大模型推理与模型部署。博士毕业于奥克兰大学 Strong AI Lab,参与 PyTorch ONNX 导出器、ONNX Runtime、ONNX Runtime GenAI 和 Microsoft PyRIT 等开源项目的贡献。