☰
大模型8位量化:LLM.int8()显存优化与推理加速
2026/11/3 20:33:00 网站建设 项目流程

简介:《巨型语言模型的8位量化:LLM.int8()中文版论文》面向自然语言处理学习者、大模型推理部署开发者及做相关毕业设计的学生,聚焦百亿级Transformer模型推理显存占用高、常规8位量化易掉点的问题。压缩包仅含1个PDF文件,大小约4.01MB,为NeurIPS 2022论文的中文译介,便于对照阅读核心公式、实验设置与结论。内容覆盖研究背景与动机、已有量化方法在小模型和超大模型上的局限、系统性异常值特征分析,以及LLM.int8()的两项关键设计:按内积使用独立归一化常数的矢量量化,和把极端异常值维度隔离到16位矩阵乘法的混合精度分解;同时给出WinoGrande、HellaSwag、PIQA、LAMBADA等零样本评测结果,说明175B参数模型量化后性能不降、可在消费者级GPU上部署OPT-175B/BLOOM。已有215人学习,适合用来建立量化直觉、梳理技术脉络并辅助实验方案设计。

1. 显存不够跑不动大模型?先看清这份 8 位量化论文解决了什么

一张 24GB 显存的卡,想塞进一个 13B 参数的语言模型,FP16 权重就要 26GB,加载到一半直接 OOM,这是很多人第一次认真考虑 8 位量化的时刻。手上这份《巨型语言模型的 8 位量化:LLM.int8()》中文版论文,讲的就是怎么把 Transformer 里前馈层和注意力投影层的矩阵乘法压到 Int8,显存砍掉近一半,而在 175B 规模上困惑度和零样本精度几乎不掉点。它适合两类人:一类是想在自己机器上把大语言模型跑起来、又不想换硬件的开发者;另一类是把量化当成软件/插件模块嵌进推理服务,或者拿来做毕业设计里加速部分的人。读完至少能搞明白一件事——为什么直接把权重除以最大值压成 8 位一定会翻车,以及翻车之后到底该怎么补救。

2. 量化地基:absmax、零点量化与一次 Int8 矩阵乘法的完整链路

在看 LLM.int8() 之前,得先把传统量化那套东西捋顺。论文的铺垫部分其实信息量很大,它把「8 位为什么会掉精度」这件事从数据类型层面讲透了。如果跳过这一层直接看混合精度分解,很容易变成照抄公式却不知道每个常数在干什么。

2.1 从 FP16 到 Int8:absmax 与零点量化到底在争什么

量化的本质是把一段连续的浮点分布映射到 [-127, 127] 这 255 个整数格子里。映射方式决定了信息损失落在哪里。

absmax 量化的做法最粗暴:取整个张量绝对值最大的那个数,除以 127 得到缩放常数s,然后所有元素除以s再四舍五入。它等价于用无穷范数做归一化。优点是计算便宜、反量化只需一次乘法;缺点是分布极度不对称时,有一半的格子被浪费。比如 ReLU 之后的激活全是非负数,[-127, 0)这一段完全空着,等于只用了 127 个格子表达整个分布,精度直接腰斩。

零点量化(zero-point quantization)就是为了填满这 255 个格子。它先把实际分布[min, max]线性拉伸到[-127, 127],再整体平移一个零点zp,让真实数据的最小值正好落在 -127 上。这样无论分布多歪,比特位都用满。论文里提到一个关键现象:在 6.7B 以上规模时,异常值几乎全是严格非对称的,要么全正要么全负,这正是零点量化在中等规模下表现更好的原因。

import torch def quantize_absmax(x: torch.Tensor): # 对称量化:整个张量共享一个缩放常数 scale = x.abs().max() / 127.0 x_i8 = torch.round(x / scale).clamp(-127, 127).to(torch.int8) return x_i8, scale def quantize_zeropoint(x: torch.Tensor): # 非对称量化:用满 [-127, 127] 全部比特位 x_min, x_max = x.min(), x.max() scale = (x_max - x_min) / 254.0 + 1e-8 zp = torch.round(-x_min / scale) - 127.0 # 零点偏移 x_i8 = torch.round(x / scale + zp).clamp(-127, 127).to(torch.int8) return x_i8, scale, zp

scale是浮点缩放常数,反量化时乘回去;zp是整数偏移,反量化时先减掉。+ 1e-8是防止全零张量导致除零,这种边界在真实权重里出现的概率不低,尤其是某些被裁剪过的 bias 附近。

代价也在这里:零点量化的矩阵乘法需要一条「先把zp加到每个元素再做整数乘」的指令,也就是常说的multiplyi16。这条指令在 x86 上有对应实现,但在 GPU 和 TPU 上通常没有,得手工展开成一堆 Int8 乘加再补 Int16/Int32 的修正项。展开之后慢得离谱,所以工程上大量场景还是退回 absmax。论文对这一点写得很直白——零点量化精度高,但真上 GPU 未必划算。

2.2 一次 Int8 矩阵乘法:缩放、累加、外积反归一化

真正的量化推理不是「把权重压成 8 位就完事」,而是一整条链路。给定隐藏状态X ∈ R^(s×h)和权重W ∈ R^(h×o),其中s是序列长度、h是特征维度、o是输出维度,完整流程是:量化输入、量化权重、整数矩阵乘累加到 Int32、用缩放常数反归一化回 FP16。

def int8_matmul_absmax(x_f16: torch.Tensor, w_f16: torch.Tensor): # x_f16: (s, h) 激活;w_f16: (h, o) 权重 x_i8, cx = quantize_absmax(x_f16) w_i8, cw = quantize_absmax(w_f16) # Int8 输入 -> Int32 累加,避免中间溢出 acc_i32 = torch._int_mm(x_i8, w_i8.t()) # 反归一化:两个标量缩放常数相乘即可 return acc_i32.to(torch.float16) * cx * cw

torch._int_mm这类算子内部走的是 GPU 上的 Int8 张量核心通路,输入必须是 8 位、输出固定 Int32。为什么累加要用 Int32?因为h动辄几千,两个 8 位整数相乘最大到 16129,乘几千次轻松溢出 Int16 的 65535。论文里反复强调的「Int8 输入、Int32 累加、FP16 输出」就是这个意思。

反归一化那一步是重点。absmax 只有两个标量常数,一个乘法搞定;但一旦缩放常数从「一个张量一个」变成「一行一个、一列一个」,反归一化就不再是标量乘法,而是一个外积:cx ⊗ cw,形状(s, 1) × (1, o) = (s, o)。这一步的显存和计算开销,正是后面向量量化要精细算账的地方。

2.3 张量级单缩放为什么会崩:一个异常值就能拉爆整个分布

理解了链路,就能理解失败模式。absmax 的死穴是「最大值主导」:假设某一行 4096 个特征里,有 4095 个值都在 0.1 附近,只有一个值是 60.0,那么scale = 60/127 ≈ 0.47。反算一下,0.1 除以 0.47 约等于 0.21,四舍五入之后是 0,也就是整整 4095 个有效信息被直接量化成零。这就是论文里说的「量化箱空了、小值被消灭」。

论文在 4.1 节给出了异常值的判定标准,三条同时满足:量级至少 6.0、影响至少 25% 的层、影响至少 6% 的序列维度。这个定义不是拍脑袋,而是为了让检测只锁定真正系统性的异常维度,而不是把偶发的单层噪声也算进去。6.7B 规模下,一个 2048 token 的序列在整个 Transformer 里能找出大约 15 万个异常值,但它们只集中在 6 个特征维度上——稀疏、系统、可预测,这三点是后面所有技巧的前提。

行量化(row-wise)和块量化(block-wise)是常规的补救办法:把缩放常数的粒度从整个张量细化到每一行或每个块,让异常值的影响被限制在局部。但论文指出了一个很要命的方向问题——隐藏状态里的异常值出现在特征维度(列)上,而行量化是按序列维度(行)给缩放的。也就是说,一行里只要有一个特征爆了,这一整行所有特征的精度还是被拖下水。这个方向对不上,是向量量化在 6.7B 之后失效的直接原因。

3. 核心方法拆解:向量量化 + 混合精度分解怎么拼出 LLM.int8()

论文的方法部分其实只有两个零件,但每个零件都有明确的适用边界,拼在一起才覆盖到 175B。理解它们各自解决哪一段问题,比记住公式重要得多。

3.1 向量量化:把缩放常数从张量级降到行列级

向量量化的思路是:既然矩阵乘法本质是一串独立的内积,那就给每个内积配一个自己的归一化常数。X的每一行配一个cx ∈ R^s,W的每一列配一个cw ∈ R^o,反归一化时用外积cx ⊗ cw还原。粒度比张量级细了s + o倍,异常值的影响被限制在它所在的那一行或那一列。

def vector_wise_int8(x_f16, w_f16): # 行缩放:每个 token 一个常数;列缩放:每个输出通道一个常数 cx = x_f16.abs().amax(dim=1, keepdim=True) / 127.0 + 1e-8 # (s, 1) cw = w_f16.abs().amax(dim=0, keepdim=True) / 127.0 + 1e-8 # (1, o) x_i8 = torch.round(x_f16 / cx).clamp(-127, 127).to(torch.int8) w_i8 = torch.round(w_f16 / cw).clamp(-127, 127).to(torch.int8) acc = torch._int_mm(x_i8, w_i8.t()).to(torch.float32) # 外积反归一化:(s,1) * (1,o) -> (s,o) return (acc * (cx * cw)).to(torch.float16)

dim=1和dim=0的选择不是随意的,它决定了缩放的粒度落在哪个方向上。keepdim=True保住广播所需的维度,否则外积那一步形状对不上。注意cx * cw这个乘法的广播结果就是(s, o),和输出同形,不需要额外的.t()。

论文的实测结论是:向量量化能把无损性能撑到 2.7B 参数。超过这个规模,异常值的量级涨得太猛,即使按行缩放,一行里那个 60.0 的特征依然会把同一行其他特征压扁。到 13B 时,即使是精度更高的零点量化也顶不住了。

3.2 混合精度分解:把 0.1% 的异常维度拎出来走 16 位

混合精度分解解决的就是「向量量化撑不住之后」那一段。既然异常值只集中在极少数特征维度上,那就别硬压,把它们单独拆出来做 16 位矩阵乘法,剩下的 99.9% 继续走 8 位。

具体做法:对输入矩阵X ∈ R^(s×h),找出所有满足「至少有一个元素量级超过阈值 α」的特征维度,构成集合O。X按O切成两块,W也按对应的行切两块。异常块做X[:, O] @ W[O, :](FP16),常规块做向量量化的 Int8 矩阵乘,最后两块结果相加。用爱因斯坦记号写就是标准的分块乘法,工程上就是两次切片加一次加法。

def llm_int8_matmul(x_f16, w_f16, alpha=6.0): # 1) 找异常特征维度:任一 token 上量级超过 alpha outlier_mask = (x_f16.abs() > alpha).any(dim=0) # (h,) idx_out = outlier_mask.nonzero().flatten() idx_reg = (~outlier_mask).nonzero().flatten() # 2) 拆成两块:异常块走 FP16,常规块走 Int8 x_out, w_out = x_f16[:, idx_out], w_f16[idx_out, :] x_reg, w_reg = x_f16[:, idx_reg], w_f16[idx_reg, :] # 3) 分别计算后相加,注意 dtype 对齐 out_out = x_out @ w_out # FP16 高精度 out_reg = vector_wise_int8(x_reg, w_reg) # Int8 主体 return out_reg + out_out.to(torch.float16)

alpha=6.0是论文给出的经验阈值,选它的依据是:把 6.0 以上的特征都当异常值处理之后,困惑度退化基本归零。idx_out和idx_reg的构造用布尔掩码而不是set,因为 GPU 上索引张量比 Python 集合快几个数量级。

这个设计的精妙之处在于成本可控。论文实测 13B 以内异常特征维度不超过 7 个,占总特征维度的 0.1% 左右,所以拆出来的 FP16 分支额外显存几乎可以忽略,而 99.9% 以上的乘加仍然跑在 8 位通路上。这也是为什么 176B 规模能拿到 1.96 倍的内存压缩,而不是理论上的 2 倍——那多出来的 0.04 就是给异常值留的余量。

3.3 阈值与预算:α、异常维度数量、额外显存怎么算

落地时最常被问的三个数:阈值取多少、会拆出多少个维度、多花多少显存。论文给的答案可以直接抄,但要知道边界在哪。

参数论文取值含义调大的后果调小的后果
α6.0判定异常值的量级阈值拆出的维度变少,精度可能掉拆出的维度变多,8 位占比下降
层覆盖阈值25%判定「系统性」需影响的层比例只抓最顽固的异常,可能漏把噪声当异常,白花显存
序列覆盖阈值6%判定需影响的序列维度比例同上同上
异常维度上限~7(13B 内)实测拆出的特征维度数——

显存预算的算法很直接:额外开销 ≈(异常维度数 / 总特征维度) × 权重显存 + 分解过程的临时张量。以 6.7B 模型、h=4096为例,7 个异常维度占比 0.17%,权重部分几乎不增加;真正吃显存的是x_out @ w_out这个 FP16 分支的中间激活,序列长度 2048 时大约是2048 × 7 × 2字节,也就几十 KB,可以忽略。

需要留意的是alpha并不是越大越好。论文在 4.3 节解释过:用了混合精度分解之后,零点量化的优势就消失了,说明剩下的分解特征是对称的。这也意味着阈值一旦设得过高、把本该拆出来的非对称异常值留在了 8 位分支里,那些值就会被 absmax 直接压成零。我一般会先按 6.0 跑一遍,再单独统计一下被判定为异常的特征维度数量和它们的中位幅值,如果中位幅值远高于 6,说明这个模型的异常值比论文里的更极端,可以把 α 下调到 5.0 试试。

3.4 从公式到可运行代码:把分解逻辑接到真实模型上

论文给的公式是单层视角,真接到模型上还得处理「哪些层要量化」这个问题。论文明确说了,量化对象是前馈层和注意力投影层——也就是q_proj / k_proj / v_proj / o_proj加上 FFN 的第一个扩展层,这些层占了 95% 的参数和 65%-85% 的计算。embedding、LayerNorm、bias、lm_head 都不动,因为它们要么参数量小,要么对精度极其敏感。

import torch.nn as nn QUANT_TARGETS = ("q_proj", "k_proj", "v_proj", "o_proj", "fc1", "gate_proj", "up_proj") def patch_linear_layers(model, alpha=6.0): """把目标 Linear 层替换成量化版本,其余层保持原样""" for name, module in model.named_modules(): if not isinstance(module, nn.Linear): continue # 只替换名字命中目标列表的层,lm_head 不在列表里,天然被跳过 if not any(t in name for t in QUANT_TARGETS): continue module.forward = make_int8_forward(module, alpha) return model def make_int8_forward(layer: nn.Linear, alpha: float): w_f16 = layer.weight.detach().to(torch.float16) # 离线量化,权重只转一次 bias = layer.bias def forward(x): out = llm_int8_matmul(x.to(torch.float16), w_f16.t(), alpha) return out + bias if bias is not None else out return forward

named_modules()遍历拿到的是完整路径名,用any(t in name ...)做子串匹配比精确匹配稳,因为不同模型的命名前缀不一样(model.layers.0.self_attn.q_proj和transformer.h.0.attn.c_attn差别很大)。权重在替换时就转成 FP16 存下来,避免每次 forward 都做一次 dtype 转换——这一步不做,吞吐能掉一半。

w_f16.t()这个转置也有讲究。nn.Linear的权重形状是(o, h),而矩阵乘法要的是(h, o),转置之后才是x @ W。转置本身在 PyTorch 里是视图操作不拷贝,但传给自定义算子时可能触发一次连续化拷贝,量大时值得用.contiguous()提前处理掉。

4. 复现排查:五个把我卡了半天的坑

论文的公式看起来干净,真跑起来问题都藏在细节里。下面五条是我在复现和对齐数值时踩过的,按「现象 → 原因 → 解决」记下来,能省不少时间。

4.1 现象:量化后输出大面积 NaN,或者第一层就精度崩塌

现象很直接——模型能加载,但一 forward 就出一堆 NaN,或者输出全是重复 token。

原因通常有两个。一是缩放常数算出了 0:某个权重张量全零(比如被剪枝过),abs().max()返回 0,除下去就是 inf。二是反归一化时的 dtype 没对齐,Int32 累加结果先乘了 FP16 常数,中间溢出。论文里 32 位累加的设计就是为了避免第二种,但自己实现时如果图省事用 FP16 接累加结果,一样会炸。

解决:缩放常数统一加1e-8下限,累加结果先.to(torch.float32)再乘常数,最后降回 FP16。另外在替换层之前跑一遍torch.isnan(w).any()和w.abs().max() == 0的检查,把有问题的层打印出来单独处理。

4.2 现象:显存不但没降,反而比 FP16 还高

这个反直觉,但很常见。加载完模型发现显存占用比原始 FP16 版本还多出几个 GB。

原因:替换forward的时候把 FP16 权重副本和原始 FP32 权重同时留在了显存里。layer.weight.detach().to(torch.float16)只是新建了张量,原来的layer.weight还挂在模块上没释放。另一个来源是分解过程的临时张量——x_f16[:, idx_out]这种高级索引会真的拷贝一份,序列长度 4096、特征维度 4096 时,一次 forward 就是几十 MB 的临时峰值。

解决:替换完 forward 之后立刻del layer.weight并把layer.weight = None,或者直接把整层的参数注册成 buffer 后置空。分解用的索引改成预计算——异常维度集合在推理时基本稳定,可以每 N 步重算一次,而不是每个 token 都重新扫一遍abs() > alpha。这一条对长序列推理的收益比想象中大。

4.3 现象:小模型量化之后反而更慢,慢了将近一倍

拿 1.3B 的模型试水,量化完发现 token/s 掉了一半,怀疑人生。

原因:论文 3.4 节其实写得很清楚,量化有额外开销,参数量小于 6.7B 的模型本来就装得进大多数 GPU,量化收益为负。开销来自三块:激活量化的实时计算、异常值检测的扫描、以及 8 位算子在小矩阵上调度开销占比过高。矩阵越小,固定开销摊得越薄不了。

解决:按参数量分档。6.7B 以下不做量化,或者只在显存真的不够时开;6.7B 到 13B 之间看场景,追求吞吐就别开,追求能装下就开;175B 这个级别量化才真正划算,论文实测大矩阵乘法能快接近两倍。

4.4 现象:换个 batch size 或序列长度,输出结果对不上

同一句话,batch=1 和 batch=8 跑出来的 logits 有可见差异,长序列比短序列差得更多。

原因:向量量化的行缩放是沿着序列维度算amax的,batch 拼进来之后,缩放常数变成了跨样本共享,一个样本里的异常值会影响同 batch 其他样本的量化精度。这在原理上就无法避免,只能减小影响。

解决:推理服务里固定 batch 策略,别让长短请求混在一个 batch;如果必须混,就在预处理阶段按序列长度分桶,把长度接近的放一起。另外别拿 batch 间的结果做严格数值比对,误差在这个量级是设计使然,不是 bug。

4.5 现象:只在某几层出问题,其他层完全正常

排查的时候发现大部分层数值对齐得很好,但总有那么两三层误差大得离谱。

原因:多半是层筛选漏了或者多选了。any(t in name for t in QUANT_TARGETS)这种子串匹配有风险——某些模型的 FFN 收缩层叫down_proj,如果目标列表里错写了proj,就会把不该量化的层也套进去。反过来,如果模型用的是融合的c_attn(qkv 打包在一起),按q_proj匹配就一个都命中不了,量化等于没做。

解决:替换之前先把所有nn.Linear的完整名字打印出来,肉眼过一遍再写匹配规则。更稳的做法是按「输入输出维度 + 层在模块树中的位置」判断,而不是靠名字。替换完再统计一下实际替换了多少层,和预期的层数对一遍,数量不对立刻停下来查。

5. 论文没细说但落地必须算的账:显存、吞吐与硬件选型

论文的重点是「不掉点」,工程落地还得算另外两笔账:到底省了多少显存,以及什么情况下会变慢。这两笔账算不清楚,很容易在小模型上白折腾一场。

5.1 显存到底省了多少:从理论 2 倍到实际 1.96 倍

权重显存的算法很朴素:参数量 × 每参数字节数。FP16 是 2 字节,Int8 是 1 字节,理论压缩比 2 倍。实际到不了,因为有三块东西不进量化:embedding 层(占比不小,尤其词表大的模型)、LayerNorm 和 bias(量小但存在)、以及异常值分支保留的 FP16 部分。

模型规模FP16 权重Int8 权重(含异常值开销)实测压缩比
6.7B~13.4 GB~6.9 GB~1.94×
13B~26 GB~13.3 GB~1.95×
175B~350 GB~179 GB~1.96×

注意这只是权重。真实推理还要加激活显存和 KV cache,长上下文场景下 KV cache 往往才是大头。所以「量化之后能不能塞进单卡」要按「权重 + 激活峰值 + KV cache」三项加起来算,只看权重容易乐观。

5.2 什么时候快、什么时候反被拖慢

论文的结论是有条件的快:175B 级别的大矩阵乘法大约快两倍,6.7B 以下因为量化开销反而变慢。这个分界线的成因值得说清楚。

8 位矩阵乘在 GPU 上的理论吞吐是 FP16 的两倍,但前提是矩阵足够大、能把张量核心喂饱。小矩阵的瓶颈在于调度和数据搬运,8 位算子省下的计算时间抵不过量化激活、检测异常值、反归一化的固定开销。序列长度也是同一个道理:s=1的自回归解码阶段,每次只算一行,量化收益几乎为零;s=2048的 prefill 阶段才是量化的主场。

所以一个常见的做法是混合策略:prefill 走量化,decode 走 FP16,或者干脆只在显存吃紧的时候才切。这个策略论文没有明说,但和它给出的实测趋势是一致的。

5.3 硬件与算子支持:为什么这条路线在消费级卡上也能跑

LLM.int8() 能用,很大程度上依赖一个现实条件:Int8 是目前 GPU 上唯一有硬件加速的 8 位数据类型。FP8 在论文写作时还没有硬件支持(论文在局限性一节里也承认没研究 FP8),所以选 Int8 不是因为它最优,而是因为它能跑。

选型时有几个点要看:目标卡的 Int8 张量核心通路是否可用(近几代的消费级和数据中心卡基本都有);算子库是否提供了成熟的 Int8 矩阵乘实现,自己手写 CUDA 内核的性价比很低;以及异常值检测那部分是不是有向量化的实现,因为它是纯 memory-bound 的操作,写不好会成为整条链路的瓶颈。

另一条容易被忽略的:混合精度分解意味着同一层里可能同时存在 8 位和 16 位的计算,如果算子库对混合 dtype 的调度不友好,会额外插入同步点。这个在单卡上不明显,多卡流水线并行时会被放大。

6. 二次开发:把这套量化接进自己的推理链路

真正动手改的时候,我不建议一上来就写完整的量化算子,而是按「先对齐、再替换、后优化」的顺序推。下面这套流程是我现在接任何量化方案的固定动作。

第一步是做接口适配,把量化逻辑挂在nn.Linear的forward上,而不是去改模型结构。好处是模型权重文件不用动,量化只是运行时行为,出问题能一键回退。替换完之后先别急着测速度,把「实际替换了多少层、跳过了多少层」打印出来对一遍,这一步能挡掉一半的低级错误。

第二步是数值对齐,分三层校验。最细一层是单元级:拿一个(2048, 4096)的随机张量和一组真实权重,对比 FP16 matmul 与量化 matmul 的最大相对误差,正常应该在 1e-2 量级;往上一层是 block 级:跑一个完整的 Transformer block,比对 hidden state 的余弦相似度,正常在 0.999 以上;最后一层是端到端:在通用网页语料验证集上算困惑度,和 FP16 基线的差距应该小于 1%。这三层里哪一层没过,问题范围立刻就能框住。

第三步才是性能优化,而且优先级要排对。按收益从大到小:权重离线量化(只做一次,必做)、异常维度集合缓存(每 N 步重算而非每 token 重算)、序列分桶减少 batch 内长度方差、最后才考虑自定义内核。

检查项通过标准不通过的典型原因
层替换数量与预期一致名字匹配漏了融合层
单元 relative error< 1e-2缩放常数溢出或 dtype 未对齐
block 余弦相似度> 0.999异常值阈值设太高
端到端困惑度差< 1%误量化了 lm_head 或 embedding
显存峰值低于 FP16 基线原始权重未释放、临时张量未复用

最后提醒一个容易被忽略的点:论文明确说了没对 attention 函数本身做 Int8 乘法,只做了投影层。原因是 attention 不含参数,省显存的收益小,而 softmax 对数值误差极其敏感。二次开发时如果有人顺手把 attention 也压了,掉点几乎是必然的,别在这上面浪费时间。

从那以后我每次接量化方案,都强制先跑一遍这三层校验再谈性能,哪怕单元测试全绿也不跳过端到端那一步——数值问题在单层上看不出来,累积到几十层之后才会以「模型突然开始胡说」的形式暴露。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询