简介GLM-OCR开源大模型部署项目源码面向需要在单张T4显卡16GB显存环境下落地多模态OCR能力的开发者和算法工程师。模型大小约2.5GB端到端识别延迟可控制在800毫秒以内除基础文字识别外还能理解文档结构、还原表格逻辑、解析数学公式适合复杂文档理解与信息抽取场景。压缩包共3个文件包含inscode项目入口文件、index.html展示页面和.gitignore配置整体仅8KB轻量精炼便于快速查看核心部署框架。已有273人学习下载。内容从架构设计、部署步骤到文本/表格/公式识别实测结果均有涉及并给出Python API调用方法和常见问题解决方案能帮助用户在资源受限环境中快速评估模型效果并集成到业务流。对于关注低延迟、高准确率开源OCR方案的技术人员这份源码包提供了清晰的入手路径和可参考的工程实践。1. GLM-OCR开源大模型部署不切字、不套模板把整张图读懂再输出同样是识别一张扫描合同传统OCR引擎跑完表格散了、手写备注全乱码你得再花十分钟逐格补录换成GLM-OCR这类开源多模态大模型它不切字、不套模板直接把整张图当上下文理解再生成文字。这不是玄学是架构换了一条路视觉编码在前文本生成在后。这篇笔记要解决的是怎么把这套模型从拿到权重开始一路部署到能批量处理图片的可用状态。目标读者是手里有文档识别需求、想把能力保留在本地机器上的开发者。看完你能拿到一份可直接改来用的部署路径显存怎么估、依赖怎么装、推理参数怎么设以及我跑数据时踩过的几个真坑。2. 部署前的硬件选型与运行环境显存怎么算依赖怎么装部署这类开源大模型和装普通软件完全不同。普通软件装完占几百兆磁盘而模型权重本身就是巨大的文件加载进显存之后要一直驻留到进程退出。这意味着你的第一个决策不是依赖版本而是机器显存够不够。2.1 显存估算按分辨率算不按参数量算很多人问的第一个问题是“这模型要多少显存”这问题本身就不完整。显存占用由两部分构成一部分是模型权重本身另一部分是运行时激活值和KV缓存后者跟输入图像的尺寸强相关。机制上GLM-OCR这类视觉语言模型会把图片切成固定大小的patch每个patch转换成视觉token进入语言模型。分辨率越高视觉token越多注意力矩阵的开销随token数近似平方增长。所以同一个模型跑一张 512×512 的小图和跑一张 4000×3000 的扫描大图显存差距可能是两倍以上。这是工程估算表按单图推理、batch1 来算实践中误差通常在 10% 以内权重规模加载精度1080p 单图峰值显存推荐起步显卡3B 级FP168~10GB12GB3B 级4bit 量化5~6GB8GB9B 级FP1622~26GB24GB9B 级8bit 量化13~16GB16GB9B 级4bit 量化9~11GB12GB我一般建议直接上 9B 级配 4bit性价比最高。理由很简单OCR 场景里 3B 的模型对复杂版面表格合并、多栏排版、模糊印章泛化明显不够而 9B 加 4bit 牺牲的精度在常见扫描件上几乎看不出来。如果你手头就是 16GB 显存的消费级显卡9B 4bit 是你最该优先尝试的配置。想当场验证的话加载模型后跑一张图再用nvidia-smi看 second-by-second 的显存占用。注意峰值出现在生成阶段不是加载阶段所以要盯着生成过程看。2.2 环境搭建与权重就位一条命令把依赖装齐环境这块踩过不少坑现在固定用这套组合问题最少Python 3.11 PyTorch Transformers Accelerate BitsAndBytes。创建环境时建议一次把包装齐避免中途缺依赖反复折腾。conda create -n glm_ocr python3.11 -y conda activate glm_ocr # 安装与本地 CUDA 版本匹配的 PyTorch我这里是 CUDA 12.1 的示例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 推理与量化相关依赖 pip install transformers accelerate bitsandbytes sentencepiece pillow第一行命令创建独立环境不污染系统其他项目第二行按你的显卡驱动选对应 CUDA 版本不确定就先跑nvidia-smi看右上角 CUDA Version。最后一行里bitsandbytes是 4bit 量化的核心依赖accelerate负责把模型自动分配到可用设备上。装完后验证一下python -c import torch; print(torch.cuda.is_available())输出 True 再继续。权重的处理方式不同渠道差别很大。如果是从源码包内附说明的下载地址拿到的分片权重结构通常长这样mkdir -p weights # 将下载好的权重目录包含 model.safetensors.index.json 等文件放到这里 ls -la weights/ # 预期看到 config.json、tokenizer、model 相关分文件这里注意权重需要完整下载后再加载缺一个分片文件会在from_pretrained时直接报错而且报错信息不一定明确。我习惯用ls -la先对比文件数量与说明里列出的分片数是否一致而不是直接跑脚本再排查半天是哪个文件损坏了。加载路径就用本地目录不需要每次都走在线下载。后面的章节所有model_path都是指向当前机器上的这个 weights 目录。提示如果你拿到的源码包自带modeling_*.py自定义模型文件优先用源码包内的加载入口不要强行套标准 Transformers 接口。这个差异我们下一章展开。3. 跑通第一张图GLM-OCR推理主脚本与参数逐行拆解环境就绪、权重就位接下来是最重要的一步让模型成功跑完一张图。这一步反复卡住的人最多多半卡在两个地方——加载类选错以及生成参数照搬对话模型配置。3.1 加载模型与图像预处理推理入口的固定写法源码包里通常已经把加载逻辑封装进了src/ocr_engine.py这类模块。但如果你要自己维护一份或者需要深度定制核心写法是明确的用 AutoProcessor 统一处理图像和文本用视觉语言模型类加载权重。# src/ocr_engine.py import torch from PIL import Image from transformers import AutoProcessor, AutoModelForImageTextToText class GLMOcrEngine: def __init__(self, model_path: str, device: str cuda): self.device device self.processor AutoProcessor.from_pretrained(model_path) self.model AutoModelForImageTextToText.from_pretrained( model_path, torch_dtypeauto, device_mapauto, low_cpu_mem_usageTrue, ).eval() def infer(self, image_path: str, prompt: str, max_new_tokens: int 512) - str: image Image.open(image_path).convert(RGB) inputs self.processor( textprompt, imagesimage, return_tensorspt, ).to(self.device) with torch.no_grad(): gen self.model.generate(**inputs, max_new_tokensmax_new_tokens, do_sampleFalse) result self.processor.batch_decode( gen[:, inputs[input_ids].shape[1]:], skip_special_tokensTrue, )[0] return result.strip()代码逻辑不复杂但有几处值得说清楚。torch_dtypeauto让模型参数保持跟权重保存时一致的精度不加这行的话某些权重默认以 FP32 载入显存直接翻倍。device_mapauto由 Accelerate 自动调度单卡就是整卡多卡会自动切分省去手动挪.to(device)的繁琐。low_cpu_mem_usageTrue避免先把模型完整载入内存再拷到显卡显存大、内存小的工作站一定要加。处理图像的细节在.convert(RGB)。很多扫描件是灰度图或带了透明通道不统一转 RGB进入视觉编码器时可能出现通道数不匹配或意外黑块。3.2 生成参数怎么设OCR任务不要照搬对话模型的配置推理参数是新手最容易翻车的地方。对话场景里模型要“发挥创造力”temperature 常调到 0.7 以上但 OCR 是提取任务要的是确定性和忠实度。我把这组参数定为默认跑过大量票据后再没动过gen_kwargs dict( max_new_tokens512, do_sampleFalse, temperature0.2, top_p0.8, )do_sampleFalse本质上是贪心解码每步取概率最高的 token这是 OCR 稳定性的第一道保险。temperature0.2是给那些必须采样的情况留的余量——某些推理框架要求 temperature 必须大于零所以设 0.2 而不是 0。top_p0.8削减低概率候选减少长文本中“自说自话”的补充内容。max_new_tokens512按 A4 单页扫描件够用整本书、整份长文档要加到 1024 甚至 2048但注意这会让生成阶段变慢显存占用也同步升高。解码时gen[:, inputs[input_ids].shape[1]:]这段是截掉 prompt token只保留模型新生成的文本。不截的话输出会带着你输入的问题再清洗反而麻烦。skip_special_tokensTrue必须加否则|endoftext|之类的结束符会被拼在结果末尾。3.3 最小批量循环先让十张图连续跑起来单张跑通后最自然的冲动是立刻循环处理几百张。别急循环里有隐性坑图片尺寸不同processor 输出的张量形状不同如果混在一个 batch 里要么报错要么强行 padding 浪费显存。最小可靠的批量循环是这样# scripts/quick_loop.py import glob from src.ocr_engine import GLMOcrEngine engine GLMOcrEngine(weights/glm-ocr-9b, devicecuda) CURRENT_PROMPT 请识别这张图片中的所有文字保留原始排版顺序不要添加任何解释。 for path in sorted(glob.glob(samples/*.jpg)): text engine.infer(path, CURRENT_PROMPT, max_new_tokens512) print(f--- {path} ---) print(text)关键点engine 实例只创建一次放在循环外面。谁会在循环里反复加载模型听起来离谱但确实有人每一张图都from_pretrained一次结果一张图跑几十秒大半时间花在重复加载上。另外sorted()保证处理顺序固定排错时能一一对应。这个阶段你该验证三件事一是单张图耗时是否符合预期9B 4bit 在 1080p 图上大约 3~8 秒二是输出文字与原文的忠实度特别关注数字和英文是否完整三是连续跑十张后显存是否增长——正常情况下显存是稳定后的平台不是持续上涨涨了说明有显存泄漏要留意是不是图像没有释放。4. 从单图到批量管线把推理脚本组织成可交付的源码结构跑通十张图不算部署完成只能算验证通过。真正要交付给团队或投入生产需要把脚本组织成可配置、可续跑、可观察的源码结构。这也是拿到一个开源项目源码后最值得花时间吃透的部分。4.1 用argparse把批量脚本变成命令行工具我把推理能力封在src/ocr_engine.py后业务入口就只保留命令行参数解析和任务编排。这样换目录、换模型、换 token 限制都不需要改代码。# scripts/run_batch.py import argparse import glob import os from src.ocr_engine import GLMOcrEngine parser argparse.ArgumentParser(descriptionGLM-OCR 批量识别) parser.add_argument(-i, --input, requiredTrue, help图片目录) parser.add_argument(-o, --output, defaultoutputs/result.jsonl, help输出文件) parser.add_argument(--model-path, defaultweights/glm-ocr-9b) parser.add_argument(--max-tokens, typeint, default512) parser.add_argument(--ext, default*.jpg, help匹配的文件通配符) args parser.parse_args() engine GLMOcrEngine(args.model_path, devicecuda) image_files sorted(glob.glob(os.path.join(args.input, args.ext))) print(f待处理文件数: {len(image_files)})argparse 的好处是每个参数都自带帮助信息团队其他人接手时python run_batch.py --help就能看懂用法不用追着问。--ext这个参数经常被忽略但真实目录里总混着 .png、.jpeg默认写*.jpg会漏掉小半文件加一个可配置项最省心。4.2 结果落盘JSON与JSONL的选择批量任务的结果落盘我默认用 JSONL 而不是 JSON。差别在于JSON 是一次性写入整个数组任务跑到一半中断之前的结果全在内存里丢光JSONL 每完成一张图就追加一行看一眼文件尾部就知道进度。import json import time with open(args.output, a, encodingutf-8) as f: for path in image_files: t0 time.time() text engine.infer(path, 请识别这张图片中的所有文字保留原始排版顺序。, args.max_tokens) record { file: os.path.basename(path), elapsed_ms: int((time.time() - t0) * 1000), text: text, } f.write(json.dumps(record, ensure_asciiFalse) \n) print(f完成: {record[file]})写入模式用a追加而不是w。后者每次打开文件会清空已有内容二次运行时前面的记录全没了。ensure_asciiFalse必须保留否则中文会变成\uXXXX转义序列人没法直接读也不好拿去比对。JSONL 的读取也很方便后续用pandas或者逐行json.loads都能消费。如果就是要给前端一次性展示最后再写个小脚本把 JSONL 转成一个 JSON 数组就行逆向转换不值得在推理阶段做。4.3 断点续跑与增量处理跑挂了一半不用重来批量处理几百张大图中途 CUDA 掉一下、服务器被人重启一下太正常了。没有断点续跑机制重跑要付出双倍时间。续跑的判断逻辑就是看输出文件里已有哪些文件名跳过它们done_files set() if os.path.exists(args.output): with open(args.output, encodingutf-8) as f: for line in f: try: done_files.add(json.loads(line)[file]) except json.JSONDecodeError: # 最后一行可能是崩溃时的残留忽略即可 continue for path in image_files: name os.path.basename(path) if name in done_files: print(f跳过已完成: {name}) continue # ... 推理与追加写入这套实现就两件事启动时扫一遍已有文件把完成的文件名收进set循环里遇到就continue。为什么用set而不是list因为文件名去重的in判断set 是常数时间list 是线性时间几百个文件感知不到差别但这是写代码的好习惯。逐行解析时损坏行要try/except兜住崩溃时最后写入的半行记录并不罕见别让它拖垮整个续跑流程。到这一步你的源码包结构基本成型glm-ocr/ ├── src/ │ └── ocr_engine.py # 模型封装 ├── scripts/ │ ├── infer_one.py # 单张图推理 │ └── run_batch.py # 批量入口 ├── configs/ │ └── prompt.yaml # 提示词与参数配置 ├── weights/ # 模型权重自备 ├── samples/ # 测试图片 └── outputs/ # 识别结果configs/prompt.yaml里的提示词不要写在代码里OCR 业务会频繁调优提示词——今天要加“保留表格结构”明天要加“手写备注单独一行”如果提示词散落在一堆代码里每改一次就要动代码、走流程、重新部署太慢了。5. GLM-OCR部署避坑显存、速度与输出格式的真实踩坑点以下几条都是我实际跑数据时踩过的有些是模型特性有些是代码习惯问题但都会导致部署失败或结果不可用。每一条都按“现象 → 原因 → 解决”整理方便你直接照着排查。5.1 显存溢出CUDA OOM现象、原因与处理步骤现象批量跑到第 N 张时控制台蹦出RuntimeError: CUDA out of memory之前跑成功的图也跟着没保存。原因分析绝大多数情况是输入图片尺寸波动大某一张超高分辨率图产生的视觉 token 远超出你的预估。文字识别里尤其常见——同一批扫描件有的图片是手机拍摄的 4000×3000有的才是规范的 2000×1500。解决思路是给图片加一道显式预处理的闸门限制最长边from PIL import Image def load_image_limited(path: str, max_long_side: int 1280): img Image.open(path).convert(RGB) w, h img.size if max(w, h) max_long_side: scale max_long_side / max(w, h) img img.resize((int(w * scale), int(h * scale))) return img这个函数把最长边压到 1280。实践下来A4 文档和票据识别在这个尺寸下文字清晰度足够显存占用却能降 30%~50%。手动 resize 前先判断避免把本来就小的图放大导致模糊。如果压完还在 OOM那就是模型精度档位太高改成 4bit 加载或者把max_new_tokens降下来——生成阶段每多一个 token 就要缓存一组 KV 状态512 变 256 能省出不少显存。5.2 推理速度越来越慢先查模型加载次数现象批量任务是每张图都变慢或者明明说好 3 秒一张实际跑到后面变成 8 秒。原因分析前者是循环里反复from_pretrained后者多半是系统内存不够GPU 和 CPU 之间开始交换显存页。曾经有个模拟项目在循环内加载模型200 张图跑了 40 分钟换成加载一次后只用了 12 分钟差距就出在重复加载。解决方式分两段。第一模型加载放到循环外严格单例第二加一个显存监控输出直接看趋势import pynvml pynvml.nvmlInit() handle pynvml.nvmlDeviceGetHandleByIndex(0) info pynvml.nvmlDeviceGetMemoryInfo(handle) print(f显存占用: {(info.used - info.free) / 1024**3:.1f} GB)如果used持续增长不回落可能是某张异常图片导致处理器缓存累积属于个别情况如果增长后立刻回落那只是 GC 延迟正常波动。另外大批量任务建议让脚本每处理 50 张自动记录一次“当前显存/耗时”连续观察两个批次就能定位速度劣化发生在哪个阶段。5.3 输出里混进「这段话的意思是…」温度与提示词的双重修正现象识别结果明显感觉“话太多”在原文之外加了解释、关联词甚至主动翻译成另一种语言再输出。原因分析这是对话模型的本性。OCR 权重虽然在识别任务上微调过但它生成机制仍然是“接续你的输入延续说话”如果没有强约束模型默认自己是助手在解说而非 OCR 引擎在誊写。解决方式是双管齐下。第一是在系统提示词里钉死角色SYSTEM_PROMPT ( 你是OCR识别引擎。你的唯一任务是输出图片中的原始文字。 不得翻译、不得解释、不得添加任何你的看法或前缀后缀。 如果图片中没有文字输出[无文字]。 )第二是把采样参数彻底关掉。有些项目里设置do_sampleTrue但没调低 temperature模型就自由发挥。OCR 这类任务贪心解码的确定性比那点“创造性”重要得多。还有一个细节max_new_tokens开太大会给模型“继续说下去”的空间明明是识别却因为 token 预算还剩很多就自行扩展内容。512 不够就用结构化的“分段输出”控制而不是无限加大 token 预算。5.4 表格数字挤成一行版面提示与后处理切分现象给定一张含多列表格的图片识别结果里数字全黏在一起2024年7月1日和¥1,200.00之间没有分隔直接变成一长串。原因分析语言模型在输出的 token 顺序上没有版面位置的强制对齐纯文本序列表达不了二维布局模型只能按自己理解的读取顺序一个接一个输出于是表格的列结构就丢失。处理办法分两层。第一层在提示词上给模型一个更明确的输出格式约定让表格有结构化载体请识别图片中的文字。如果存在表格使用Markdown表格输出每个单元格内容用|分隔第一行为表头。实测这个提示词能让模型把单元格用竖线串起来比让它自由输出稳定得多。第二层是后处理兜底比如识别结果中相邻两个数字间没有分隔但原图明显是两列时用正则做粗切分import re text re.sub(r(?\d)(?\d{4}年), | , text)这个正则在日期后插入分隔符属于领域特定的补救办法。更通用的做法是尽量在提示词阶段解决问题后处理正则只负责兜底不要试图用正则修复一切版面问题那样只会陷入一个又一个特例。5.5 输出的JSON解析失败截取与容错现象要做结构化输出提示词里明确要求“只输出JSON”但结果还是混了额外文字或者json.loads直接抛异常。原因分析模型凭训练惯性会在 JSON 外面包一层 json 或加“好的识别结果如下”这类话术另一种常见情况是max_new_tokens太小JSON 被截断在中间花括号不闭合。解决方案是“解析前必做清洗”用正则把 JSON 片段从完整输出里截出来import re import json def extract_json(text: str) - dict: match re.search(r\{.*\}, text, re.S) if not match: raise ValueError(未找到 JSON 片段) return json.loads(match.group(0)){.*}用了贪婪匹配加re.S让点号匹配换行会从第一个{截到最后一个}能容忍模型在外层加的话术。截出来后仍然可能解析失败这时要检查原始输出是否被截断——打印一下len(text)和文本末尾如果末尾是“断头”的字段名问题在max_new_tokens而不是解析逻辑。这五条坑有一个共同点都不是模型“坏”了而是部署者与模型之间的交互方式不对。改提示词、调参数、换预处理大多数都能在现场解决不需要去重新训练。6. 进阶低显存量化加载与一套稳定的输出约定6.1 用bitsandbytes做4bit量化加载显存不够时4bit 量化是部署的最后一张牌。加载方式只多一个参数model AutoModelForImageTextToText.from_pretrained( weights/glm-ocr-9b, load_in_4bitTrue, bnb_4bit_compute_dtypefloat16, bnb_4bit_use_double_quantTrue, )bnb_4bit_compute_dtypefloat16表示反量化后的计算仍在半精度下做保留速度use_double_quantTrue再压一层量化误差。注意一点量化主要作用于语言模型部分如果你发现识别生僻字、手写字的能力下降优先把图片预处理的分辨率提上去比关掉量化划算得多——分辨率带来的收益通常比精度损失更直接。6.2 一套稳定的OCR提示词与我的使用习惯跑了大半年后我把提示词固定成下面这个模板配置文件和代码里保持一致系统你是OCR识别引擎。只输出图片中的原始文字不翻译、不解释。 用户请识别这张图片。要求 1. 按阅读顺序输出 2. 表格用Markdown格式 3. 手写内容放在[备注]区域 4. 无法识别的字符用□代替最后这条“无法识别用□代替”看似奇怪实际很关键默认行为是模型会把看不清的字“猜”出来猜错了你根本不知道。让它明确输出 □你才知道哪些地方需要人工复核这比悄悄出错再事后对账可靠得多。一个值得讲出来的教训替某公司的票据模拟项目做批量识别时第一版全量跑完发现手写备注区域全部缺失——不是没识别而是模型按“阅读顺序”跳过了它认为不重要的手写内容。后来在提示词里增加“手写内容单独输出到备注区”重跑一遍召回率大幅度提升。这件事提醒我OCR 大模型的输出质量一半在模型权重里另一半在你写的那段提示词里。此后每接一个新类型的文档我先拿十张代表性图片调整提示词再决定要不要动推理参数。希望这位帮你把 GLM-OCR 部署到可用的状态也让后续的调优之路少几个夜晚的排查。本文还有配套的精品资源点击获取