国产加速卡跑 ChatGLM3-6B:MLU370-M8 环境、显存与排错手册
发布时间:2026/10/28 11:37:37 锦皓数字建站

上周有人在群里丢过来一句话手里有一台插着 MLU370-M8 的机器想把 ChatGLM3-6B 跑起来做个内部知识问答的 Demo问有没有现成的趟坑记录。这个问题其实很典型——国产加速卡这两年在推理场景铺得很快但围绕 ChatGLM3-6B 这类开源对话模型的落地资料大多还停留在装好驱动就能跑的层面真正卡住人的往往是版本链、算子覆盖和显存估这三个点。所以我干脆把过去一段时间在 MLU370-M8 上折腾 ChatGLM3-6B 的完整过程整理成一份运行手册从环境自检、模型加载、代码改造到显存核算、性能调优和报错排查一条线走完。这份内容适合手上有这类加速卡、想在本地做模型验证或私有化推理服务的开发者也适合刚接手国产硬件适配任务、需要一份能照着敲的操作清单的朋友。文中所有命令和参数都以能在单机单卡上跑通、能把输出质量验证清楚为目标不追求极限吞吐先保证正确性再谈优化。1. 先把预期摆正这套组合能跑什么、跑不动什么很多适配工作死在第一步——不是环境装不上而是目标定错了。以为插上卡、装好包、模型就能像在常规 GPU 上那样一行from_pretrained跑起来结果发现 attention 实现不匹配、某些算子直接报 not implemented、量化方法压根没有对应后端。所以动手之前先把能跑什么和跑不动什么划清楚后面每一步都会顺畅很多。1.1 硬件与模型的匹配关系先算一笔账ChatGLM3-6B 是一个 62 亿参数级别的中英双语对话模型权重以半精度保存时约 12GB 出头。MLU370-M8 属于该加速卡系列里的高端型号单卡显存容量在几十 GB 档位具体容量以设备规格书为准不同配置有差异。这意味着在不做任何量化的情况下单卡塞下完整权重是没问题的甚至还能留出相当一部分余量给 KV Cache 和运行时开销。真正的压力不在权重而在长上下文。ChatGLM3-6B 支持 8K 上下文如果并发几条 8K 请求KV Cache 会迅速膨胀。好在它用了分组查询注意力GQAnum_key_value_heads只有 2head_dim是 12828 层结构每 token 的 KV 开销比常规多头注意力小一个数量级。后面第 4 章我会把公式列出来这里先给个结论单条 8K 会话的 KV 占用在几百 MB 级别远小于权重本身所以显存规划的重心是权重 一份合理的 Cache 上限 框架预留。至于跑不动什么我踩过的边界有这么几条一是基于 CUDA 自定义内核的量化方案比如依赖特定 GPU 内核库的 4bit 量化直接搬过来是行不通的二是 FlashAttention 这类针对特定硬件指令集优化的注意力实现在国产卡上通常没有对应版本三是某些依赖torch.cuda命名空间的第三方工具链比如部分推理框架的自动设备探测逻辑会直接把你判定成没有可用加速设备。这三件事决定了后面的代码改造方向。1.2 三类读者的上手路径同样是跑 ChatGLM3-6B不同人的起点差别很大路径也不该一样。第一类是环境已经就绪的读者。机器上驱动、固件、工具链都装好了深度学习框架的加速版本也能import成功。这类人可以直接跳到第 3 章看模型加载和代码改造第 2 章当成核对清单扫一眼即可。第二类是机器刚到只有操作系统的读者。需要从驱动安装开始把四层版本链条一层层对齐。这类人建议严格按第 2 章顺序来别跳步尤其别先装 Python 侧的包再去补驱动那样出一堆undefined symbol会让你怀疑人生。第三类是想评估这套方案值不值得用的读者。可能手上有一批这类卡想先跑个小模型验证可行性和性能量级。这类人可以重点看第 4 章的估算方法和第 5 章的报错速查表用最小成本判断这条路走不走得通再决定要不要投入做工程化。无论哪一类我都建议先在一台机器上把单卡、单请求、短上下文这条最短路径跑通能稳定输出正确回答了再去碰多卡、并发、量化这些进阶内容。一次性把复杂度拉满出问题的时候你根本分不清是环境、代码还是模型的问题。2. 环境搭起来之前先确认版本链条对得上我见过太多明明按文档装的却跑不起来的案例追到最后全是一个原因版本链条上有一环对不上。加速卡这类硬件的软件栈是分层的层与层之间有严格的对应关系不像普通 Python 库那样随便升个版本还能凑合。2.1 四层版本依赖一层错就全盘报错从上到下梳理这套软件栈大致是这么四层层级内容作用出错时的典型表现第一层驱动内核态与设备通信设备节点缺失、监控命令看不到卡第二层固件卡上运行的底层程序驱动能识别但初始化失败、偶发超时第三层工具链算子库、运行时、编译工具LD_LIBRARY_PATH相关问题、算子找不到第四层框架适配包深度学习框架的加速后端import报错、is_available()返回 False第一层和第二层通常打包在同一个安装包里装完重启即可。真正容易出错的是第三层和第四层的对应关系——工具链有大版本号框架适配包必须匹配对应的大版本同时框架适配包又要匹配上游 PyTorch 的具体小版本。举个我实际遇到的例子机器上工具链是较新的版本但框架适配包装的是给旧版上游框架编译的结果import之后所有算子调用都抛undefined symbol。当时第一反应是装漏了依赖折腾半天才意识到是版本错配。所以我的做法是安装之前先把三个东西记下来工具链版本号、上游框架版本号、适配包版本号然后去官方发布的对应表里核对一遍确认三者互相兼容再动手。注意不要用pip install --upgrade去顺手升级框架适配包。它会把你原本对齐的版本链条打乱而且升级后往往只报一个很含糊的错误。另外提一句环境变量的事。工具链装好后通常需要把库路径加到LD_LIBRARY_PATH把设备可见性控制变量设好。这一步千万不要图省事直接写进/etc/profile就完事因为多用户机器上不同用户可能需要不同版本的栈。我习惯在项目目录里放一个env.sh每次开新终端先source一下好处是版本切换特别干脆坏处是容易忘——所以我后来又加了个前置检查脚本里第一句就是检测关键库能不能被ldd找到。2.2 容器与裸机两条路线怎么选适配这类硬件容器和裸机各有各的舒服。裸机路线的优势是排查方便。所有日志、所有库路径都是直白的出问题可以直接ldd看依赖、strace看系统调用。缺点是环境容易被污染装了几个不同版本的框架之后想回退很难。容器路线的优势是环境隔离、可复制。原厂一般会提供预装好工具链和框架适配包的镜像拉下来挂上设备节点就能用省掉大量安装时间。但要注意两件事一是设备节点和工具链目录必须挂载进容器否则容器里is_available()永远是 False二是宿主机驱动版本必须和镜像里的工具链版本兼容镜像再新也救不了宿主驱动太老。我更推荐的组合是宿主机只装驱动和固件保持干净所有工具链和框架放在容器里用不同 tag 的镜像对应不同项目。这样做的好处是当你同时维护两三个不同版本要求的项目时不用来回折腾宿主环境。# 启动容器的思路示意具体镜像名与设备节点路径以现场为准 docker run -it --rm \ --device/dev/设备节点0 \ --device/dev/设备节点1 \ -v /usr/local/工具链目录:/usr/local/工具链目录 \ -v /data/models:/data/models \ -v $(pwd):/workspace \ --shm-size16g \ 镜像名:tag \ bash--shm-size这个参数容易被忽略。默认共享内存只有 64MB跑多进程数据加载或某些推理框架时会莫名其妙卡死加到 16G 基本不会有问题。2.3 十分钟环境自检脚本环境装完之后别急着上模型先跑一段自检。这一步能省掉后面大量到底是环境问题还是代码问题的纠结。# check_env.py import os import sys import torch def check(name, fn): try: result fn() print(f[OK] {name}: {result}) return True except Exception as e: print(f[FAIL] {name}: {type(e).__name__}: {e}) return False # 1. 上游框架版本 check(torch version, lambda: torch.__version__) # 2. 加速后端能否导入 ok check(import torch_mlu, lambda: __import__(torch_mlu).__version__) # 3. 设备是否可用 if ok: check(device available, lambda: torch.mlu.is_available()) # 4. 设备数量 if ok: check(device count, lambda: torch.mlu.device_count()) # 5. 最小张量运算 def minimal_op(): x torch.randn(64, 64, dtypetorch.float16).to(mlu:0) y torch.randn(64, 64, dtypetorch.float16).to(mlu:0) z torch.matmul(x, y) torch.mlu.synchronize() return tuple(z.shape) if ok: check(matmul on device, minimal_op) # 6. 关键算子覆盖度抽查 def op_coverage(): dev torch.device(mlu:0) a torch.randn(2, 8, 8, dtypetorch.float16, devicedev) b torch.randn(2, 8, 8, dtypetorch.float16, devicedev) out [] out.append(torch.softmax(a, dim-1).sum().item()) out.append(torch.nn.functional.scaled_dot_product_attention(a, b, b).mean().item()) out.append(torch.nn.functional.silu(a).mean().item()) out.append(torch.nn.functional.layer_norm(a, (8,)).mean().item()) torch.mlu.synchronize() return len(out) if ok: check(op coverage probe, op_coverage) print(\n上游框架:, torch.__version__, | Python:, sys.version.split()[0])这段脚本有两点值得说。第一import torch_mlu这一步很关键它会注册设备后端不导入的话torch.device(mlu:0)直接报错而且报错信息很不直观。第二最后那个算子覆盖度抽查我特意挑了 softmax、缩放点积注意力、SiLU 激活、LayerNorm 这四个——它们正好是 ChatGLM3 前向计算里最核心的几个算子。如果这一项挂了模型基本不可能跑通早点知道比跑到一半崩掉强。如果自检里device available是 False先别怀疑代码按这个顺序查设备节点在不在、当前用户有没有权限访问设备节点、驱动版本和工具链版本是否兼容、LD_LIBRARY_PATH有没有缺。权限问题特别常见新机器上用户默认不在对应的设备访问组里加进去重新登录就好了。3. 模型加载与代码改造让 6B 模型真正落到卡上环境跑通之后到了最关键的一环——让模型真正在这张卡上跑起来。这里有个认知要先建立ChatGLM3-6B 的参考实现默认是围绕常规 CUDA 生态写的直接搬到国产卡上大部分代码没问题但有几个关键位置必须改。3.1 attention 实现方式的取舍这是整个适配过程里最容易踩坑的地方。ChatGLM3 的注意力层在实现上做了分支可以走融合的 FlashAttention 路径可以走 PyTorch 原生的缩放点积注意力路径也可以走最朴素的 eager 实现——手动算QK^T、做 mask、softmax、再乘 V。在国产加速卡上我的建议很明确优先走 eager也就是最朴素的那条路。原因不在性能而在可控性。融合路径依赖具体硬件的高度定制内核国产卡上要么没有要么版本对不上原生缩放点积注意力路径虽然框架层支持但底层是否已经映射到加速卡上、映射得好不好各版本差异很大一旦出问题报错信息会非常抽象。eager 路径的好处是每一层都用最基础的矩阵乘、加法、掩码、softmax 组合出来这些算子在加速卡上覆盖度最高几乎不会遇到 not implemented。代价是性能有损失但对先跑通、再优化的节奏来说这是最划算的选择。设置方式有两种取决于你拿到的代码版本。比较新的实现走配置项from transformers import AutoConfig config AutoConfig.from_pretrained(MODEL_PATH, trust_remote_codeTrue) config._attn_implementation eager # 强制走朴素实现如果代码里是自定义的配置字段改成对应的字段名即可本质都是告诉模型别用融合内核。也可以在config.json里直接加这一项省得每次加载都改{ _attn_implementation: eager }注意这个字段必须放在config.json的顶层不要塞进torch_dtype那类嵌套结构里。曾经有人把它写到了错误层级结果配置被静默忽略模型还是走了默认路径。改完之后可以验证一下加载模型后打印model.config._attn_implementation确认是eager再往下走。3.2 设备声明、精度与 dtype 的细节设备声明看起来简单.to(mlu:0)就完事但有几个细节值得单独说。第一import torch_mlu必须在任何设备相关操作之前执行。它负责把设备后端注册进框架如果顺序反了torch.device(mlu:0)会报不认识这个设备类型。第二精度控制。ChatGLM3-6B 权重的参考精度是半精度。这里有个常见误区同时用torch_dtypetorch.float16和调用.half()。两个都写会触发警告而且可能引入不必要的类型转换开销。选一个就行我习惯用torch_dtype语义更清晰。第三low_cpu_mem_usageTrue建议加上。它会在加载权重时逐片映射而不是先在内存里拼一个完整副本。12GB 的权重在内存紧张的机器上不加这个参数容易触发 OOM Killer。这个参数依赖accelerate库需要提前装好。第四位置编码相关的计算建议保持在 float32。ChatGLM3 的旋转位置编码里涉及三角函数计算半精度下在长序列上会出现精度累积误差表现出来就是前几百个 token 回答正常越往后越容易胡言乱语。这个细节在参考实现里通常已经处理好了——它会在计算完再转回半精度——但如果你自己改过前向代码一定要留意这一点。第五model.eval()和torch.inference_mode()别漏。前者关掉 dropout 和归一化的训练行为后者减少显存占用并提速。别用torch.no_grad()函数式推理模式在新版本框架里更彻底。3.3 一个可直接复现的最小推理脚本下面这段是我压到最小的可复现版本从加载到流式输出一条龙改一下路径就能跑# run_chatglm3.py import os os.environ.setdefault(TOKENIZERS_PARALLELISM, false) import time import torch import torch_mlu # noqa: F401 必须导入以注册设备后端 from transformers import AutoTokenizer, AutoModelForCausalLM, AutoConfig MODEL_PATH /data/models/chatglm3-6b DEVICE mlu:0 assert torch.mlu.is_available(), 加速后端未就绪请先跑环境自检 torch.mlu.set_device(0) config AutoConfig.from_pretrained(MODEL_PATH, trust_remote_codeTrue) config._attn_implementation eager tokenizer AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_codeTrue) t0 time.time() model AutoModelForCausalLM.from_pretrained( MODEL_PATH, configconfig, trust_remote_codeTrue, torch_dtypetorch.float16, low_cpu_mem_usageTrue, ) model model.to(DEVICE).eval() torch.mlu.synchronize() print(f模型加载耗时: {time.time() - t0:.1f}s) query 用一个生活化的例子解释什么是 KV Cache。 history [] with torch.inference_mode(): # 预热一次排除首次算子选择与编译开销 _ model.chat(tokenizer, 你好, history[]) torch.mlu.synchronize() t1 time.time() response, history model.chat( tokenizer, query, historyhistory, max_length8192, top_p0.8, temperature0.6, ) torch.mlu.synchronize() print(f单轮耗时: {time.time() - t1:.2f}s) print(response)流式版本把model.chat换成model.stream_chat它是个生成器逐段吐出文本with torch.inference_mode(): for chunk, history in model.stream_chat( tokenizer, query, historyhistory, max_length8192, top_p0.8, temperature0.6, ): print(chunk, end, flushTrue)这段脚本里有三个设计意图值得说明。第一个是预热。加速卡的算子库通常带有自动调优机制第一次执行某个形状的算子时会做一次选择耗时会明显偏高。如果不预热就直接计时你会得到首轮 8 秒、第二轮 1 秒这种毫无参考价值的数据。预热一次之后测出来的才是稳态性能。第二个是torch.mlu.synchronize()。加速卡上的计算是异步下发的Python 侧执行完model.chat只是把任务排进队列就返回了。不加同步你测到的耗时其实只是下发任务的时间。这一条在 GPU 上也是同理但在加速卡上更容易被忽略因为很多人在 GPU 上已经养成同步习惯了。第三个是max_length和采样参数。max_length是输入加输出的总长度上限不是输出上限这一点经常被误解。top_p0.8和temperature0.6是这类对话模型比较常用的组合偏低一点的温度让回答更聚焦。跑通这一段之后先把输出质量验一遍。我的做法是准备十几个固定的测试问题覆盖事实问答、多轮指代、代码生成、长文本总结这几类每次改完环境或参数都跑一遍对比。这套小测试集看起来粗糙但能极快地发现模型跑起来了但输出质量崩了这类隐蔽问题——比如算子精度不足导致的重复输出光看单句是看不出来的。4. 显存怎么算、性能怎么调跑通只是及格线接下来的问题一定是能扛多少并发和为什么这么慢。这两件事本质上都是显存和算力的分配问题先算清楚再调比瞎试参数有效得多。4.1 KV Cache 与峰值显存的估算方法很多人对显存的直觉是模型多大就占多大这个认知在大模型推理上是错的。真正的峰值显存由四部分构成权重、KV Cache、激活值、框架预留。其中占比最大的两块是权重和 KV Cache。先把 KV Cache 的公式写清楚。每个 token 在每一层需要存一份 K 和一份 V所以每 token 每层 KV 字节数 2 × kv_heads × head_dim × dtype_bytes代入 ChatGLM3-6B 的实际参数kv_heads 2head_dim 128半精度下dtype_bytes 2。算下来每层每 token 是 1024 字节也就是 1KB。模型有 28 层那么每 token KV 总量 1KB × 28 28KB这个数字小得有点反直觉但它就是 GQA 带来的收益。对比一下同等规模的传统多头注意力模型kv_heads是 32 而不是 2每 token 的 KV 就是 448KB是前者的 16 倍。再算总量。单条会话跑满 8K 上下文8192 × 28KB ≈ 229MB四条并发跑满也就不到 1GB。这意味着显存规划的压力几乎全在权重上。所以显存预算可以这么估项目估算值说明权重约 12.1GB62 亿参数 × 2 字节KV Cache单条 8K约 0.23GB按上文公式激活值峰值约 1~3GB与 batch 和输入长度强相关框架与算子库预留约 2~4GB各版本差异较大留足余量合计单条 8K约 16~20GB实际以监控命令读数为准这个表里的框架预留那一项我建议不要抠。加速卡的运行时、算子库、通信缓冲区都要占显存而且不同版本占用差异不小。见过有人按理论值卡着边界配机器结果每次多来一个请求就 OOM白白浪费了大量调试时间。实测时怎么拿到真实数字用驱动自带的设备监控命令看占用。加载完模型看一眼、跑完一轮推理再看一眼两个数字的差值才是推理的真实增量。顺带说监控命令的采样间隔建议调到 100ms 以内不然峰值会被平均掉你看到的是一个偏低的数字。4.2 吞吐优化批处理、流式与并发性能优化这件事先要分清你在优化哪个指标。是首 token 延迟还是整体吞吐这两者的优化方向经常相反。首 token 延迟主要由 prefill 阶段决定也就是把整段输入一次性编码成 KV 的过程。这个阶段是计算密集型的输入越长越慢。用户感知最强烈的就是这一段——点了发送之后要等多久才看到第一个字。对于 6B 模型在加速卡上的情况几百 token 的输入首 token 延迟在百毫秒到秒级这个区间比较常见具体取决于算子覆盖度和输入长度。解码速度则是另一回事。每生成一个 token都要把全部权重读一遍是典型的访存密集型任务。所以解码速度基本被显存带宽锁死这时候批量推理的收益会非常明显——因为一批 N 条请求共享同一次权重读取相当于把带宽成本摊薄了。我的优化顺序是这样的第一步把输入和输出都控制在够用的长度。输入长度直接决定 prefill 耗时输出长度决定总耗时的后半段。做内部知识问答时我一般把检索到的上下文控制在 1K 到 2K token输出上限定在 512这个配置下响应体验和成本都能接受。第二步开流式输出。模型生成的时间没变但用户感知的等待时间大幅缩短因为第一个字很快就出来了。这一条纯属体验优化不改变实际算力消耗。第三步再做批处理。批处理要自己写调度把同时到达的请求攒一小批一起跑。这里有个坑批处理会让首 token 延迟变长因为要等批里最慢的那条。所以攒批窗口别开太大几十毫秒量级通常就比较平衡。至于多卡说实话单卡跑 6B 推理的显存是够的多卡带来的收益主要在并发量上。如果只是做验证没必要一上来就上多卡多卡会引入设备映射、通信、负载均衡一堆问题复杂度陡增。真要上多卡注意别指望自动设备映射能识别加速卡——那套逻辑是按常规 GPU 生态写的很可能把模型全塞到 CPU 上去。稳妥做法是手动指定层分配或者干脆起多个单卡实例前面挂个负载均衡。4.3 量化路线的可行性判断量化的诱惑永远很大4bit 量化能把 12GB 权重压到 4GB 左右显存需求直接砍半。但在国产加速卡上做量化我的态度是先确认工具链再决定路线。第一个要判断的是现成的低比特库能不能用。主流的那几个 4bit 量化库底层依赖特定硬件的定制内核直接搬过来基本跑不通。你可能会遇到库能导入、量化能执行但推理时算子报错这种情况因为量化权重反量化那一步用到了加速卡上没实现的内核。第二个要判断的是模型自带的量化接口。ChatGLM 系列参考实现里提供了 8bit 和 4bit 的量化方法但它们同样是面向特定硬件生态写的在加速卡上不能直接假设可用。要试就单独起一个测试脚本别在正式代码里改跑不通的话回退成本低。第三个也是最靠谱的路线用原厂提供的量化工具链做离线量化。这类工具一般支持训练后量化把模型离线转成一个专门格式的推理模型运行时用配套的推理引擎加载。这条路的好处是有官方保证、性能优化到位坏处是流程和 PyTorch 原生方式不一样需要额外的转换步骤而且量化后精度需要自己验。我的建议是分场景选择。做验证、做内部 Demo直接用半精度就行显存够省事精度无损。做规模化部署、并发压力大再考虑量化而且要预留一轮精度对比的工作量——我见过不少量化之后模型能说话但开始瞎编的情况尤其是涉及数字、日期这类精确信息时。5. 报错现场实录与排查手册前面几章是顺风顺水的路径这一章才是真正干活的地方。下面这些是我在实际操作中反复遇到、并且每次都花了不少时间才定位的问题整理成速查表放在前面细节展开在后面。5.1 高频报错速查表报错现象最可能的原因排查动作torch.mlu.is_available()为 False后端未导入 / 驱动未就绪 / 无设备权限先import torch_mlu再查设备节点与用户组undefined symbol: ...框架适配包与上游框架版本错配核对三者版本对应表重装适配包cannot open shared object file库路径未加入环境变量检查LD_LIBRARY_PATH用ldd逐个验RuntimeError: ... out of memory显存超限用监控命令看占用降 batch 或降max_length算子not implemented走了融合内核或未覆盖的算子切回 eager 实现避免融合路径加载到一半进程被杀内存不足加low_cpu_mem_usageTrue检查交换分区输出重复、循环、乱码精度问题或位置编码溢出位置编码用 float32检查算子是否回落到 CPU速度异常慢秒级/token算子被降级到 CPU 执行检查是否有.cpu()隐式调用看告警日志关于最后一条我想多说两句。算子静默降级是这类适配里最隐蔽的问题因为程序不报错就是慢。判断方法很简单跑一个小算子用监控命令看设备利用率。如果利用率几乎为零但耗时很长那就是在 CPU 上算的。ChatGLM3 里有几个地方容易出现这种隐式降级比如 attention 的掩码构造、某些张量的repeat和reshape组合。定位到具体位置后把这些小张量也搬到设备上或者改成等价但更好的算子组合。5.2 输出质量异常的定位方法能跑和跑对是两件事而后者更难判断因为输出是自然语言你很难一眼看出是模型本身的问题还是精度的问题。我的定位方法分三步走。第一步用一个确定性的输入做对照。把do_sample关掉温度设为 0或者用贪心解码同一个问题跑两次看输出是否完全一致。如果两次不一样说明有随机性没关干净或者有非确定性算子这本身就是个问题。第二步换一个极简的等价计算做交叉验证。比如你不确定 attention 部分算得对不对就手工构造一个很小的输入比如 3 个 token、单层用 numpy 在 CPU 上把同样的计算走一遍和加速卡上的结果逐元素对比。这一步听起来麻烦但它是最能定位到具体哪一行出问题的方法。我就靠这个方法抓到过一次掩码构造中类型不匹配导致的问题——半精度掩码在极端负值处溢出成了infsoftmax 之后整个分布就乱了但模型依然会吐字只是答非所问。第三步做长序列回归。准备几个需要长上下文的问题比如给一段 3000 字的材料让它总结。短序列正常、长序列崩基本就是位置编码或 KV Cache 的问题。注意验证精度时torch.allclose的容差不要设太死。半精度计算本来就有误差逐元素对比时用相对误差判断别用绝对相等。5.3 几条踩过坑才明白的经验最后分享几条文档里不会写、但确实能省时间的经验。第一条模型权重放本地盘。12GB 的权重如果放在网络盘上加载时间可能从几十秒变成几分钟而且中途网络抖动一下就要重来。有条件的话放本地 NVMe 上差一点的用本地 SSD 也行。第二条把环境自检脚本和最小推理脚本当成两个固定资产维护。每次换机器、换版本、换人员接手先跑这两个脚本。它们能覆盖掉八成以上的环境问题剩下的才是真问题。我在不同项目间来回切换时这两个脚本省下的时间远超写它们的时间。第三条改动要一次只改一处。版本、代码、参数同时动跑挂了之后你不知道是哪一处导致的。这个道理人人都懂但赶进度的时候最容易违反结果就是返工。第四条多轮对话的 history 是有代价的。每一轮都要把历史拼进输入第二轮输入长度是第一轮的两倍第五轮就是五倍。做多轮应用时要有历史截断策略否则跑到第七八轮就会突然变得极慢甚至超长。我一般保留最近三轮加上一个滚动摘要。第五条别在第一次就跑 benchmark。加速卡的算子库有自动选择和缓存机制第一次执行的耗时没有参考价值。先跑十几轮预热再取稳定后的平均值这才能反映真实性能。这个坑我踩过当时测出来单 token 要 800ms差点判定这套卡不能用后来发现是预热没做够。第六条也是我觉得最重要的任何时候都要保留一条能在 CPU 上跑通的基线。当加速卡上结果异常时把同一个模型在 CPU 上以 float32 跑一遍对比输出。如果 CPU 上也一样那是模型或输入的问题如果 CPU 正常而卡上异常那才是适配的问题。这条基线能帮你快速划清责任边界避免在错误的方向上浪费时间。这套流程走下来从一台只有操作系统的机器到能稳定输出正确回答的 ChatGLM3-6B 服务顺利的话小半天遇到版本问题可能要多花一天。真正花时间的从来不是敲命令而是判断现在这个问题属于哪一层。等你把第 2 章的自检和第 5 章的速查表用熟之后绝大多数问题的定位时间能压到十分钟以内。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。