news 2026/9/23 9:40:35

PaddleNLP experimental.model_utils 模块深度解析:FasterPretrainedModel 加速推理基类与量化 Scale 加载器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PaddleNLP experimental.model_utils 模块深度解析:FasterPretrainedModel 加速推理基类与量化 Scale 加载器

PaddleNLP experimental.model_utils 模块深度解析:FasterPretrainedModel 加速推理基类与量化 Scale 加载器

【免费下载链接】PaddleNLPEasy-to-use and powerful LLM and SLM library with awesome model zoo.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP

本篇文章以 docs/zh/source/paddlenlp.experimental.model_utils.rst 所对应的 API 文档页为主线,结合 paddlenlp/experimental/model_utils.py 的完整源码实现,系统讲解 PaddleNLP 实验性(experimental)模型工具模块:包括面向推理加速的FasterPretrainedModel基类、服务于 INT8/FP8 量化推理的激活与权重 Scale 加载器,以及 FP8 block 量化相关的底层辅助函数。读完本文,你将掌握这些工具类在 llama、qwen2、deepseek_v2 等加速模型量化推理流程中的定位、核心参数语义与调用方式。

一、模块定位:为"加速推理 + 量化部署"服务的实验性工具集

在 PaddleNLP 中,paddlenlp.experimental目录承载的是面向 LLM 推理优化的实验性实现,它通过 paddlenlp/experimental/init.py 中的from .model_utils import *将工具类直接暴露为paddlenlp.experimental.model_utils命名空间。对应 API 文档页通过 Sphinx 的automodule指令自动生成:

.. automodule:: paddlenlp.experimental.model_utils :members: :no-undoc-members:

也就是说,文档页所列出的全部公开成员即模块源码中__all__声明的四个导出符号:

__all__ = ["FasterPretrainedModel", "ActScalesLoader", "WeightScalesLoader", "PerTensorWeightScalesLoader"]

除此之外,模块内还定义了CacheScaleLoaderget_dequant_weightblock_quant_to_fp8等内部工具,被多个加速模型实现直接 import 使用(见后文"仓库内实际使用场景")。

从功能上可以把整个模块划分为三块:

功能块关键符号用途
加速推理模型基类FasterPretrainedModel继承标准PretrainedModel,提供静态图导出(to_static)与权重/资源配置加载能力
量化 Scale 加载器ActScalesLoaderWeightScalesLoaderPerTensorWeightScalesLoaderCacheScaleLoader将 PTQ 产出的 JSON scale 文件装载为模型推理所需的 NumPy scale 数组
量化底层辅助函数get_dequant_weightblock_quant_to_fp8load_vocabularyFP8 反量化、block 量化与词表加载

二、FasterPretrainedModel:面向推理的预训练模型基类

FasterPretrainedModel继承自 paddlenlp/transformers 中的PretrainedModel(见from paddlenlp.transformers import PretrainedModel),在保留标准模型加载/保存能力的同时,新增了面向推理加速的静态图导出能力。

2.1 to_static:将动态图模型导出为静态图

def to_static(self, output_path): self.eval() # Convert to static graph with specific input description model = paddle.jit.to_static( self, input_spec=[paddle.static.InputSpec(shape=[None, None], dtype=core.VarDesc.VarType.STRINGS)] ) paddle.jit.save(model, output_path) logger.info("Already save the static model to the path %s" % output_path)

关键点说明:

  • 先切到 eval 模式self.eval()确保导出时关闭 dropout、BN 等训练期行为,保证导出模型与推理语义一致;
  • paddle.jit.to_staticInputSpec:通过shape=[None, None]dtype=core.VarDesc.VarType.STRINGS的输入描述把模型转换为静态图。这里输入被声明为两维字符串张量,对应推理引擎(如自研的 Paddle Inference 加速算子)约定的原始 token 输入格式;
  • paddle.jit.save(model, output_path):将静态图模型保存到指定路径,便于后续用 C++/Inference 引擎加载执行,避开动态图逐算子调度的开销,这正是"Faster"推理的关键一步。

2.2 from_pretrained:支持内置/社区/本地三种加载来源

该基类重写了类方法from_pretrained,完整兼容PretrainedModel.from_pretrained的语义,pretrained_model_name_or_path参数可接受三类取值:

  1. 内置预训练模型名(如bert-base-uncased):从cls.pretrained_init_configurationcls.pretrained_resource_files_map中查表获取配置与资源文件;
  2. 社区贡献模型名(如yingyibiao/bert-base-uncased-sst-2-finetuned):走resolve_file_path下载解析流程;
  3. 本地目录(如./my_bert/):要求目录内包含权重文件model_state.pdparams与配置文件model_config.json

加载流程还支持若干扩展关键字参数:

参数默认值说明
cache_dirNone模型文件缓存目录
from_hf_hubFalse是否从 Hugging Face Hub 拉取资源
from_aistudioFalse是否从 AI Studio 拉取资源
subfolder""资源文件所在子目录

其内部实现还有几处值得注意的细节:

  • 配置与类签名匹配检查:读取model_config.json后,通过init_class字段判断配置属于基类还是派生类(带 head 的模型),并据此拆分配置给base_model_class与派生模型构造;
  • 词表文件兜底校验:无论哪种加载路径,都会弹出vocab_file并断言非空,否则报错"The vocab file is None. Please reload the class ... with pretrained_name."
  • 权重前缀自适应:加载model_state.pdparams时,依据base_model_prefix自动剥离或补齐前缀,使得"基类权重"与"派生类(带 head)模型"都能正确装载;同时记录missing_keys/unexpected_keys并通过logger.info输出;
  • 动态/静态模式分支:动态图模式下调用set_state_dict并返回模型实例;静态图模式下返回(model, state_to_load)元组供上层继续处理。

2.3 save_pretrained 与 save_resources:一键保存完整模型

def save_pretrained(self, save_dir): assert not os.path.isfile(save_dir), "Saving directory ({}) should be a directory, not a file" os.makedirs(save_dir, exist_ok=True) self.save_model_config(save_dir) # 保存 model_config.json if paddle.in_dynamic_mode(): file_name = os.path.join(save_dir, list(self.resource_files_names.values())[0]) paddle.save(self.state_dict(), file_name) # 保存 model_state.pdparams else: logger.warning("Save pretrained model only supported dygraph mode for now!") self.save_resources(save_dir)
  • save_pretrained会依次保存模型配置(model_config.json)、模型状态(model_state.pdparams)以及 tokenizer 等附属资源,产出的目录可直接作为后续from_pretrainedpretrained_model_name_or_path使用,形成"保存 → 重新加载"闭环;
  • save_resources遍历resource_files_names,将初始化时传入的资源文件(如词表)从源路径拷贝到保存目录,通过os.path.abspath比较避免源目录与目标目录相同造成冗余拷贝;
  • 注意:静态图模式下保存仅输出告警日志(logger.warning),因此建议在动态图(dygraph)模式下调用保存接口。

2.4 load_vocabulary:词表文件加载

模块级函数load_vocabulary(filepath)以及FasterPretrainedModel上的同名静态方法,均以"每行一个 token"的格式解析词表文件,构建token -> 行号索引的字典,供加速推理场景快速构造 tokenizer 词表映射。

三、量化 Scale 加载器:把 PTQ 产物装进推理引擎

大模型量化部署(如 W8A8、FP8、KV Cache INT8)通常先离线做 PTQ(后训练量化)得到各张量的 scale,再在推理时加载这些 scale 参与反量化计算。model_utils提供的四个 Loader 类统一承担了这一职责。它们有共同的构造形态:

Loader(scale_json_file_path="xxx_scales.json", key_map_dict=..., num_of_layers=...)

其中key_map_dict描述"逻辑 scale 名称 → JSON 文件中的 key 模板"的映射,模板中的#会被替换为具体层号i,从而把 JSON 中按层组织的 scale 提取为[num_of_layers, ...]形状的 NumPy 数组;某层缺失时填充-1.0作为"跳过/无效层"标记。

3.1 ActScalesLoader:激活 Scale 加载器

class ActScalesLoader: def __init__(self, scale_json_file_path="act_scales.json", key_map_dict=None, num_of_layers=None): ... for scale_type, key_template in self.key_map.items(): self.scale[scale_type] = np.full([num_of_layers], fill_value=-1.0, dtype="float32") for i in range(num_of_layers): if key_template.replace("#", str(i)) in self.scale_dict.keys(): self.scale[scale_type][i] = 1 / self.scale_dict[key_template.replace("#", str(i))]

要点:

  • 默认读取act_scales.json,输出形状为[num_of_layers]的一维数组;
  • 对 JSON 中存储的激活最大值做了取倒数处理(1 / scale_dict[...]),即直接得到推理计算所需的反量化因子,避免推理侧再做除法。

3.2 WeightScalesLoader:支持 QKV/FFN1 拼接的权重 Scale 加载器

class WeightScalesLoader: def __init__(self, scale_json_file_path="weight_scales.json", key_map_dict=None, num_of_layers=None, concat_qkv=False, concat_ffn1=False):

相比ActScalesLoader,它输出二维数组[num_of_layers, n],其中n由 JSON 中对应层 scale 的长度决定(自适应 per-channel 粒度),并新增两个拼接开关:

参数默认值行为
concat_qkvFalse为 True 时生成qkv_weight_scale,即按[q, k, v]顺序用np.concatenate拼出 QKV 融合权重(对应融合了 Q/K/V 三个投影的矩阵)的 scale
concat_ffn1False为 True 时生成ffn1_weight_scale,拼接ffn1_1_weight_scale(gate)与ffn1_2_weight_scale(up)两段 scale

这两个开关对应推理侧常见的权重融合优化:把多头注意力的 Q/K/V 三个权重拼接为一个大 GEMM 的 QKV 权重、把 FFN 的 gate/up 拼接为大 GEMM 权重,以提升算子吞吐,scale 必须同步拼接才能与融合权重对齐。

3.3 PerTensorWeightScalesLoader:per-tensor 粒度的权重 Scale 加载器

class PerTensorWeightScalesLoader: def __init__(self, scale_json_file_path="weight_scales.json", key_map_dict=None, num_of_layers=None):

WeightScalesLoader的区别在于形状推断更加通用:它从 JSON 中第一个有效层的 scale 值推断scale_shapenp.array(...).shape),输出形状为(num_of_layers,) + scale_shape,因此既能容纳 per-channel([hidden])也能容纳 per-tensor([1])等不同粒度的 scale。

其最重要的行为是自动推导 QKV scale:当qkv_weight_scale不在 JSON 中时,自动按如下规则计算:

self.scale["qkv_weight_scale"][i] = max( abs(self.scale["q_weight_scale"][i]), abs(self.scale["k_weight_scale"][i]), abs(self.scale["v_weight_scale"][i]), )

即取 q、k、v 三者 scale 绝对值的最大值作为融合 QKV 权重的统一 scale,保证融合后反量化不溢出,同时省去离线合并 JSON 的步骤。该 Loader 正是 FP8 量化路径(quant_type"fp8")使用的加载器。

3.4 CacheScaleLoader:KV Cache INT8 的 Scale 加载器

class CacheScaleLoader: def __init__(self, scale_json_file_path="cache_scales.json", key_map_dict=None, num_of_layers=None, num_heads=None, num_key_value_heads=None):

针对 KV Cache 静态 INT8 量化(cachekv_int8_type == "static")场景:

  • 依据 key 名中的cache_k/cache_v区分 K、V,并额外生成对应的cache_k_out_scale/cache_v_out_scale输出反量化 scale;
  • 对 GQA(Grouped-Query Attention,num_heads != num_key_value_heads)做了处理:按range(0, num_heads, num_heads // num_key_value_heads)步长采样头部,即对每组 Query 头共享同一个 KV 头 scale 做对应;否则直接遍历num_key_value_heads
  • 内部使用127.0 / scale得到输入 scale、1.0 / scale得到输出 scale,符合 INT8 对称量化x_q = x / scale、反量化x = x_q * scale的约定。

四、Scale 文件与 key 模板的约定:以 Qwen2 为例

Loader 依赖key_map_dict把逻辑名映射到 PTQ 产物 JSON 的真实 key。仓库为不同模型维护了对应映射文件,例如 paddlenlp/experimental/transformers/qwen2/ptq_scales_map.json(W8A8)与 paddlenlp/experimental/transformers/qwen2/ptq_fp8_scales_map.json(FP8):

{ "act_scale": { "qkv_in_scale": "qwen2.layers.#.self_attn.q_proj.activation_quanter", "out_linear_in_scale": "qwen2.layers.#.self_attn.o_proj.activation_quanter", "ffn1_in_scale": "qwen2.layers.#.mlp.gate_proj.activation_quanter", "ffn2_in_scale": "qwen2.layers.#.mlp.down_proj.activation_quanter" }, "weight_scale": { "q_weight_scale": "qwen2.layers.#.self_attn.q_proj.weight_quanter", "k_weight_scale": "qwen2.layers.#.self_attn.k_proj.weight_quanter", "v_weight_scale": "qwen2.layers.#.self_attn.v_proj.weight_quanter", "out_linear_weight_scale": "qwen2.layers.#.self_attn.o_proj.weight_quanter", "ffn1_1_weight_scale": "qwen2.layers.#.mlp.gate_proj.weight_quanter", "ffn1_2_weight_scale": "qwen2.layers.#.mlp.up_proj.weight_quanter", "ffn2_weight_scale": "qwen2.layers.#.mlp.down_proj.weight_quanter" }, "cachekv_scale": { "cache_k_scale": "qwen2.layers.#.self_attn.cachek_matmul.activation_quanter", "cache_v_scale": "qwen2.layers.#.self_attn.cachev_matmul.activation_quanter" } }

可观察到:

  • #即层号占位符,qwen2.layers.0.self_attn.q_proj.weight_quanter这样的 key 会被提取为第 0 层的 q 权重 scale;
  • act_scale的 value 指向各线性层输入侧的activation_quanter(激活量化器),weight_scale指向weight_quanter(权重量化器);
  • W8A8 映射中 FFN1 拆为ffn1_1/ffn1_2(gate/up)两个 key,正好配合WeightScalesLoader(concat_ffn1=True)做拼接;FP8 映射则使用ffn1_0_weight_scale等命名,配合PerTensorWeightScalesLoader使用。

注意ptq_fp8_scales_map.jsonweight_scale不含ffn1_1/ffn1_2而含ffn1_0,两套映射命名差异体现了 W8A8 与 FP8 两条量化路径各自独立的 scale 布局。

五、在仓库中的实际调用链:量化 Scale 如何进入推理模型

上述 Loader 在实验性加速模型中被系统化调用,典型的入口是各模型modeling.py中的set_quant_scale()。以 paddlenlp/experimental/transformers/qwen2/modeling.py 为例,其流程为:

  1. 读取映射文件:根据self.quant_type选择ptq_fp8_scales_map.jsonptq_scales_map.json(shift-smooth 变体为ptq_scales_map_shift_smooth.json),并解析出 act/weight/cachekv 三组 key 映射;
  2. 定位 scale 文件:通过resolve_file_path(self.quant_model_path, "act_scales.json")等找到 PTQ 产物;当tensor_parallel_degree > 1且非single_card_ptq时,改用带 rank 后缀的文件(如act_scales_{tensor_parallel_rank}.json),保证每个张量并行 rank 只加载自己分片对应的 scale;
  3. 构造 Loader 并注入模型
    • FP8 路径:ActScalesLoader+PerTensorWeightScalesLoader,将结果转 float32 后赋给self.transformer_block.weight_scales/act_scales
    • W8A8 路径:ActScalesLoader+WeightScalesLoader(concat_qkv=True, concat_ffn1=True),随后在注入时对qkv_out_linear_ffn1_weight_scaleffn2各类 scale 除以127.0 * 127.0 * act_scale组合归一化,并写入qkv_out_scaleslinear_out_scalesffn1_out_scalesffn2_out_scales等逐层缓冲区(同时处理张量并行切分与重排);
    • KV Cache 静态 INT8:CacheScaleLoader读取cachekv_scales.json(含 rank 后缀变体),将cache_k_scale/cache_v_scale/cache_k_out_scale/cache_v_out_scale写入cache_k_scales等缓冲区;
  4. 一致性处理set_quant_scale整体包在@paddle.no_grad()下,且对缺失层(scale 为-1)跳过拷贝,保持与离线量化时跳过的层一致。

同样的调用模式也出现在 paddlenlp/experimental/transformers/llama/modeling.py(约 L863-L991 区域,FP8 用ActScalesLoader+PerTensorWeightScalesLoader,W8A8 用ActScalesLoader+WeightScalesLoader,缓存用CacheScaleLoader)以及 paddlenlp/experimental/transformers/mistral/modeling.py、paddlenlp/experimental/transformers/mixtral/modeling.py 中,说明这套加载器是各加速模型共享的通用量化装载基础设施。

六、FP8 底层辅助函数:get_dequant_weight 与 block_quant_to_fp8

除 Loader 外,模块还提供两个 FP8 相关的底层函数。

6.1 get_dequant_weight:利用 FP8 GEMM 实现反量化

def get_dequant_weight(w, w_s=None, dtype=None, weight_block_size=[128, 128]): if w_s is None: return w assert weight_block_size == [128, 128] from paddlenlp_ops import per_token_group_quant try: from paddlenlp_ops import cutlass_fp8_fp8_half_block_gemm_fused as fp8_block_gemm_fused except: assert False, "fp8_block_gemm_fused only supported on sm90" eye = paddle.eye(w.shape[0], dtype=paddle.float32) x_q, x_s = per_token_group_quant(eye, group_size=weight_block_size[1], transpose_scale=True, quant_max_bound=448.0, quant_min_bound=-448.0) out = fp8_block_gemm_fused(x_q, w.t(), x_s, w_s.t(), bias=None, transpose_x=False, transpose_y=True, output_dtype=dtype, act="identity") return out

其设计思路是"以 GEMM 代反量化":当w_s为 None 时直接返回原始权重;否则构造单位矩阵并做 per-token-group 量化得到(x_q, x_s),再调用 CUTLASS FP8 block GEMM 算子cutlass_fp8_fp8_half_block_gemm_fused与量化权重相乘,利用 FP8 Tensor Core 硬件加速完成解码,从而复用 FP8 算子的高性能路径。同时代码通过assert显式声明:该算子仅支持weight_block_size == [128, 128],且cutlass_fp8_fp8_half_block_gemm_fused仅支持 SM90 架构(Hopper),在其他平台上会直接断言失败。该函数在 paddlenlp/experimental/transformers/deepseek_v2/modeling.py 中被调用(约 L775 对 KV 投影、L1020-L1033 对 gate/up/ffn2 投影),用于在推理前把 FP8 权重复原成目标 dtype。

6.2 block_quant_to_fp8:128×128 分块量化到 FP8

def block_quant_to_fp8(x: paddle.Tensor, weight_block_size=[128, 128], eps=1e-6): assert weight_block_size == [128, 128] assert x.ndim == 2 m, n = x.shape x_padded = paddle.zeros((cell_div(m, 128) * 128, cell_div(n, 128) * 128), dtype=x.dtype) x_padded[:m, :n] = x x_view = x_padded.view([-1, 128, x_padded.shape[1] // 128, 128]) x_amax = x_view.cast(paddle.float32).abs().max(axis=[1, 3], keepdim=True).clip(min=eps) x_scaled = (x_view * (448.0 / x_amax)).to(paddle.float8_e4m3fn) x_q = x_scaled.view_as(x_padded)[:m, :n].contiguous() x_s = (x_amax / 448.0).view([x_view.shape[0], x_view.shape[2]]) return x_q.cast(paddle.float8_e4m3fn), x_s

实现要点:

  • 将输入按 128×128 分块,不足部分先paddle.zeros补齐(cell_div即向上取整除法(x + y - 1) // y),量化后再裁回原始尺寸;
  • 每个 block 用其绝对最大值(abs().max,下限eps=1e-6防除零)作为 amax,按 FP8 E4M3 的量化上界 448 缩放后转paddle.float8_e4m3fn,返回量化张量x_q与对应 scalex_s(形状为[块行数, 块列数])。这正是 FP8 block 量化推理算子所需的标准输入格式。

七、总结与实践建议

paddlenlp.experimental.model_utils是 PaddleNLP 加速推理与量化部署链路的公共基础设施,其价值可归纳为:

  • FasterPretrainedModel打通了"训练/微调动态图模型 → 静态图导出 → Inference 引擎推理"的加速路径,同时完整继承标准PretrainedModel的加载保存语义,可直接替换使用;
  • 四个 Scale Loader把离线 PTQ 产出的act_scales.jsonweight_scales.jsoncachekv_scales.json等标准化装载为模型可用的 NumPy scale 数组,并内置 GQA 头映射、QKV/FFN1 拼接、per-tensor 兜底、张量并行 rank 分片等推理侧必需的处理逻辑;
  • FP8 辅助函数get_dequant_weightblock_quant_to_fp8)与paddlenlp_ops中的 CUTLASS 算子配合,把 FP8 量化/反量化融入 GEMM 计算,实现 SM90 硬件上的高性能 FP8 推理。

若要在自己的量化推理流程中复用这些工具,建议:先确认模型的ptq_scales_map.json(或 FP8 变体)key 模板与 PTQ 导出格式一致;再按量化类型选择WeightScalesLoader(W8A8,配合concat_qkv/concat_ffn1)或PerTensorWeightScalesLoader(FP8);最后参考set_quant_scale的注入方式把 scale 写入模型对应缓冲区。同时注意get_dequant_weightblock_quant_to_fp8目前仅支持[128, 128]分块及 SM90 架构,跨平台/跨分块尺寸使用前需确认算子支持情况。

【免费下载链接】PaddleNLPEasy-to-use and powerful LLM and SLM library with awesome model zoo.项目地址: https://gitcode.com/gh_mirrors/pa/PaddleNLP

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 9:38:38

Go语言核心特性与开发实践全解析

1. Go语言核心特性与设计哲学 Go语言由Google工程师Robert Griesemer、Rob Pike和Ken Thompson于2007年开始设计,2009年正式发布。作为一门为现代分布式系统而生的编程语言,Go在设计上做出了许多突破性的选择。 Go语言三大设计原则:简单性&…

作者头像 李华
网站建设 2026/9/23 9:38:18

光谱成像技术:从原理到应用的全方位解析

1. 光谱成像技术全景解析在遥感探测、环境监测和工业检测领域,光谱成像技术正经历着从单一波段到全波段覆盖的革命性发展。作为从业十余年的光学工程师,我见证了从传统全色相机到超广谱成像设备的迭代历程。这些技术并非简单的升级替代,而是针…

作者头像 李华
网站建设 2026/9/23 9:38:11

Switch版GTA5安装教程:作弊菜单与武器飞机调出全攻略

1. Switch版GTA5安装与作弊菜单的完整解析1.1 为什么Switch版GTA5值得折腾Switch版GTA5这个话题,在玩家圈子里一直热度不减。很多人第一次听到“Switch上跑GTA5”会觉得不可思议,毕竟这游戏当年是给PS3和Xbox 360设计的,后来又在PS4、PS5和PC…

作者头像 李华
网站建设 2026/9/23 9:35:12

2026外贸网站建站系统有哪些?想做外贸生意的老板看过来

2026外贸网站建站系统有哪些?想做外贸生意的老板看过来!艾瑞咨询发布的《2025年中国中小企业出海数字化报告》显示,当前超六成外贸企业将独立站作为品牌出海的核心阵地,兼具低成本、快上线、全功能的SaaS建站系统,正成…

作者头像 李华