pt&&onnx&&openvino&&gguf

经过前面的学习,我们大概都知道有这些不同的模型了,也知道一些他们的运用场景,然后我想再学习一下他们都是由什么组成的

pytorch文档

pt

构成

首先,如图

从 PyTorch 1.6.0 开始,.pt 文件实际上是一个 ZIP64 压缩档案

data.pkl

  • 这是通过 Python 的 pickle 模块序列化的对象
  • 包含:
    • 是传递给 torch.save() 对象的 pickle 序列化结果
    • 包含模型的元数据和结构信息,但不包含实际的张量存储数据
    • 具体包括:
      • 模型的类定义引用
      • 张量的形状(shape)、数据类型(dtype)、步长(stride)等元数据
      • 张量之间的视图关系(view relationships)
      • state_dict 的字典结构(如果保存的是 state_dict)
      • data/ 中存储文件的引用指针

作用:

    • 描述模型的架构和组织结构
    • 记录如何重建模型对象的蓝图
    • 指向实际数据在 data/ 文件夹中的位置

data 文件夹

  • 包含所有的 torch.Storage 对象(实际的张量数据)
  • 每个存储(storage)是一个单独的文件
  • 文件通常以数字命名(0, 1, 2, 3...)
  • 存储内容包括:
    • 模型的权重(weights)
    • 偏置(biases)
    • 批量归一化的运行统计量(running_mean, running_var)
    • 所有可学习参数和持久化缓冲区

作用:

  • 存储模型的实际参数数据(张量的底层存储)
  • 实现存储共享(storage sharing)
  • 如果多个张量共享同一个底层存储(如视图关系),只保存一份数据
  • 节省空间并保持张量间的关系

version

  • 包含 PyTorch 的版本信息
  • 用于兼容性检查

保存过程

  1. 序列化元数据 → data.pkl
    1. 将 state_dict 的字典结构、键名等 pickle 化
    2. 记录每个张量的元信息
  1. 提取存储对象 → data/
    1. 将每个独立的 torch.Storage 提取出来
    2. 保存为单独的文件
  1. 打包成 ZIP64 → .pt 文件
    1. 将所有文件压缩归档

加载过程

  1. 解压 ZIP64 归档
  2. 读取 data.pkl 重建字典结构和元数据
  3. 从 data/ 加载存储,填充实际数据
  4. 重建张量,恢复视图关系

拿我们HIMLoco训练的pt模型来举例

import torch

model_data = torch.load('/home/extra/zhy/桌面/IsaacGym_Preview_4_Package/HIMLoco-main/himloco_gym/logs/rough_go1/good/model_10000.pt', 
                        map_location='cpu')

print("所有键名:", list(model_data.keys()))

# 常见的键名模式
possible_keys = ['model_state_dict', 'model', 'actor', 'critic', 'policy', 
                 'state_dict', 'network', 'actor_critic']

for key in possible_keys:
    if key in model_data:
        print(f"\n找到模型权重键: '{key}'")
        weights = model_data[key]
        if isinstance(weights, dict):
            total_params = sum(v.numel() for v in weights.values() if isinstance(v, torch.Tensor))
            print(f"   参数量: {total_params:,} ({total_params/1e6:.2f}M)")
            print(f"   键数量: {len(weights)}")
            print(f"   键名: {list(weights.keys())[:]}")

# 如果上面没找到,遍历所有键
print("\n" + "="*60)
print("检查所有键是否包含张量:")
print("="*60)
for key, value in model_data.items():
    if isinstance(value, dict):
        tensor_count = sum(1 for v in value.values() if isinstance(v, torch.Tensor))
        if tensor_count > 0:
            total_params = sum(v.numel() for v in value.values() if isinstance(v, torch.Tensor))
            print(f"  '{key}': 包含 {tensor_count} 个张量,总参数 {total_params:,}")

那么什么是键呢?

一个键 = 一个可学习参数张量(一个可调节的数字参数),而这个参数就是我们最后需要得到的东西

就是我输入特定格式的东西,根据他神经网络的结构加上这些参数各种计算,最后得出特定格式输出

所以说 data.pkl 是整体结构(结构 + 键名 + 地址索引)

data文件夹里面就是具体的数值

TorchScript

这东西也是一个用来部署的一个pt,他本质上是PyTorch 的编译版本

他的推理是比正常的pytorch要快的,也可以支持cpp推理,但是比onnx还是差一些

主要还是和PyTorch 生态绑定的

这是解压缩后的

code文件夹

存储 TorchScript 编译后的计算图代码

包含了模型的静态计算图表示

计算图可以理解为提前把整个网络的结构写好,这样用的时候就不用一行行解析python代码了,所以也实现了跨语言,速度也更快,当然,快不过onnx

其他

serialization_id

TorchScript 特有的序列化标识符

constants.pkl

存储常量数据(如形状、配置)

data.pkl

主数据文件(元数据、结构信息)

byteorder

字节序信息(大端/小端)

version

格式版本号

特点就是

  • 计算图编译优化
  • 在 C++ 环境中加载
  • 不包含优化器状态(无法继续训练)
  • 结构固定(无法修改网络)

onnx

他是Protobuf 二进制

结构

ONNX 模型 = 计算图 (Computational Graph)

计算图包含 4 大部分:
┌─────────────────────────────────────────────────
│  1️⃣ 输入 (Inputs)                               
│     比如:270 维传感器数据                        
├─────────────────────────────────────────────────
│  2️⃣ 输出 (Outputs)                              
│     比如:12 维关节控制命令                       
├─────────────────────────────────────────────────
│  3️⃣ 节点 (Nodes) - 计算步骤                      
│     比如:矩阵乘法、激活函数、加法...            
├─────────────────────────────────────────────────
│  4️⃣ 初始化器 (Initializers) - 参数值             
│     比如:30 个键的具体数值 (weight/bias)         
└─────────────────────────────────────────────────

pt是框架和具体数值是分开的,onnx是混在一起的

还是以HIMLoco的为例子,代码

import onnx

# 加载模型
model = onnx.load('/home/extra/zhy/桌面/IsaacGym_Preview_4_Package/HIMLoco-main/himloco_gym/logs/rough_go1/good/model_10000.onnx')

# 查看基本信息
print("="*60)
print(f"模型版本:{model.opset_import[0].version}")
print(f"生产者:{model.producer_name}")
print("="*60)

# 查看输入
print("\n📥 输入:")
for inp in model.graph.input:
    shape = [d.dim_value for d in inp.type.tensor_type.shape.dim]
    print(f"  名称:{inp.name}")
    print(f"  形状:{shape}")

# 查看输出
print("\n📤 输出:")
for out in model.graph.output:
    shape = [d.dim_value for d in out.type.tensor_type.shape.dim]
    print(f"  名称:{out.name}")
    print(f"  形状:{shape}")

# 查看节点 (计算步骤)
print("\n🔧 计算节点:")
print(f"  总节点数:{len(model.graph.node)}")
for i, node in enumerate(model.graph.node[:10]):  # 只显示前 10 个
    print(f"  [{i}] {node.op_type}: {node.input} → {node.output}")

# 查看参数 (初始化器)
print("\n🎛️ 参数 (初始化器):")
print(f"  总参数数量:{len(model.graph.initializer)}")
total_params = 0
for init in model.graph.initializer:
    shape = list(init.dims)
    params = 1
    for s in shape:
        params *= s
    total_params += params
    print(f"  - {init.name}: {shape} ({params:,} 参数)")

print(f"\n💾 总参数量:{total_params:,} ({total_params/1e6:.2f}M)")

所以就很明确了

  • 整个模型 = 一个计算图
  • 每个节点 = 一个计算操作
  • 初始化器 = pt的键值
  • 输入,输出

关于键值

因为我这里onnx是纯推理,而且由于HIMLoco的框架设计,就导致实际的onnx中的初始化器个数比pt的键值少

所以这就是为啥 onnx 不能和 pt 一样继续训练的原因了

openvino

结构

他的整个结构类似于onnx

这里我是用yolo的pt转成openvino的

  • best.xm l就于是整个神经网络的计算图,也就是整个策略的结构
    • 这里要注意的是,虽然这里叫计算图,但是他只负责静态拓扑和操作语义,权重被刻意剥离到独立的 best.bin 中
  • best.bin 就是 权重的具体信息 也就是 data 文件夹里面的东西
  • metadata.yaml 就是用来描述模型的辅助文件
    • 记录导出时间、框架来源、量化方式、输入输出名称等,便于后续加载或调试

gguf

gguf就是很多本地大模型用的,比如ollama,LMstudio啥的

这里我以 gemma-3n-E4B-it-Q4_K_M.gguf 为例子分析//

from llama_cpp import Llama

# 你的 GGUF 文件路径
model_path = r"E:\ai\models\lmstudio-community\gemma-3n-E4B-it-text-GGUF\gemma-3n-E4B-it-Q4_K_M.gguf"

try:
    # n_gpu_layers=0 表示纯 CPU 加载,只用来读信息,不推理
    llm = Llama(
        model_path=model_path,
        n_gpu_layers=0,          # 只读元数据,不加载权重到 GPU
        verbose=False,            # Ture的话会打印加载时的 GGUF 元数据
        n_ctx=512,               # 随便设小一点
        load_format="gguf"       # 强制 GGUF
    )
    
    # 加载成功后,llm.metadata 就是字典形式的元数据
    print("\n" + "="*60)
    print("GGUF 元数据(llama-cpp-python 解析):")
    for key, value in sorted(llm.metadata.items()):
        if isinstance(value, (list, tuple)) and len(value) > 10:
            print(f"  {key}: <list/tuple with {len(value)} items>")
        else:
            print(f"  {key}: {value}")

    print("\n模型基本信息:")
    print(f"  架构: {llm.metadata.get('general.architecture', '未知')}")
    print(f"  名字: {llm.metadata.get('general.name', '未知')}")
    print(f"  量化类型: {llm.metadata.get('general.file_type', '未知')} (或从文件名推断 Q4_K_M)")
    print(f"  上下文长度: {llm.metadata.get('llama.context_length', '未知')}")

except Exception as e:
    print(f"加载失败: {e}")

从名字开始,gemma-3n-E4B-it-Q4_K_M.gguf

gemma

  • 模型家族:Google 的 Gemma 系列(开源轻量模型)。
  • Gemma 是 Google DeepMind 推出的高效开源模型家族,继 Gemma 1、Gemma 2 之后,这是第 3 代变体。

3n

  • 版本标识:Gemma 3n(Gemma 3 的 “n” 变体)。
  • “n” 代表 nested / nested MatFormer 架构(Matryoshka-like Transformer),这是 Gemma 3n 的核心创新:支持动态提取子模型(比如从 8B 里切出 4B 或 2B 有效参数的子网络)。
  • 它让模型在边缘设备上更灵活:可以根据硬件资源选择不同“深度/宽度”。

E4B

  • Effective 4 Billion(有效参数量约 4B)。
  • 虽然底层总参数可能接近 6.9B–8B,但通过 MatFormer + 共享 KV + 交替投影(AltUp)等技术,实际计算和内存开销相当于传统 4B 模型
  • 这是 Gemma 3n 系列最关键的卖点:把大模型能力塞进小内存。

it

  • Instruct-Tuned(指令微调版)。
  • 说明这个模型经过了指令跟随(instruction tuning)和对话优化,不是原始的 base 模型。
  • 适合聊天、问答、工具调用、角色扮演等任务,输出更听话、更符合人类偏好。

Q4_K_M

  • 量化类型Q4_K_M(4-bit 量化 + K-means clustering + Medium 变体)。
  • 这是 llama.cpp / GGUF 生态中最受欢迎的量化方案之一:
    • Q4:4-bit 每权重
    • K:使用 K-means 聚类优化量化误差
    • M:中等质量/速度平衡(比 Q4_K_S 稍大但质量更好,比 Q5_K_M 小但更快)
  • 实际效果:在 4-bit 下,性能损失很小(通常 perplexity 只涨 5–10%),但文件大小和内存占用减少 60–70%。
============================================================
GGUF 元数据(llama-cpp-python 解析):
  # gemma3n.* 系列字段(Gemma 3n 专有架构参数)
  gemma3n.altup.active_idx: 0                # AltUp机制的激活索引,用于模型中的自适应层上采样
  gemma3n.altup.num_inputs: 4                # AltUp机制的输入数量
  gemma3n.attention.head_count: 8            # 注意力头的数量,这里是8个头
  gemma3n.attention.head_count_kv: 2         # Key-Value头的数量,使用GQA(Grouped Query Attention)
                                             # 2个KV头对应8个查询头
                                             
  gemma3n.attention.key_length: 256          # 注意力机制中Key向量的长度
  gemma3n.attention.layer_norm_rms_epsilon: 0.000001        
  # RMSNorm(Root Mean Square Normalization)的 epsilon 值 = 1e-6。用于稳定 LayerNorm 计算,避免除零
  gemma3n.attention.shared_kv_layers: 15.000000      # 共享KV的层数,跨层共享Key-Value缓存以节省内存
  gemma3n.attention.sliding_window: 512      # 滑动窗口注意力的大小,限制每个token只能关注最近的512个token
  gemma3n.attention.value_length: 256        # 注意力机制中Value向量的长度
  gemma3n.block_count: 35                    # Transformer块的总数,即模型有35层
  gemma3n.context_length: 32768              # 最大上下文长度,可以处理32768个token
  gemma3n.embedding_length: 2048             # 主嵌入向量的维度
  gemma3n.embedding_length_per_layer_input: 256    # 每层输入的嵌入维度
  gemma3n.feed_forward_length: 16384         # 前馈网络(FFN)的隐藏层维度
  gemma3n.rope.freq_base: 1000000.000000     # RoPE(旋转位置编码)的频率基数
  
  # general.* 系列字段(通用信息)
  general.architecture: gemma3n              # 模型架构类型,基于Google的Gemma 3n架构
  general.basename: gg-hf-gm_gemma           # 模型基础名称
  general.file_type: 15                      # GGUF文件类型编码,对应Q4_K_M量化
  general.finetune: 3n-E4B-it                # 微调版本信息,3n架构,4B参数,instruction-tuned
  general.name: Gg Hf Gm_Gemma 3n E4B It     # 模型完整名称
  general.quantization_version: 2            # 量化版本号
  general.size_label: 6.9B                   # 模型大小标签,约6.9B参数
  general.type: model                        # 文件类型,这是一个模型文件
  
  # 分词器信息
  tokenizer.chat_template: {{ bos_token }}
  # 对话模板,定义了如何格式化对话历史,支持:
      # 系统消息处理
      # 用户/助手角色交替检查
      # 多模态内容(音频、图像、文本)
      # BOS/EOS token插入
{%- if messages[0]['role'] == 'system' -%}
    {%- if messages[0]['content'] is string -%}
        {%- set first_user_prefix = messages[0]['content'] + '

' -%}
    {%- else -%}
        {%- set first_user_prefix = messages[0]['content'][0]['text'] + '

' -%}
    {%- endif -%}
    {%- set loop_messages = messages[1:] -%}
{%- else -%}
    {%- set first_user_prefix = "" -%}
    {%- set loop_messages = messages -%}
{%- endif -%}
{%- for message in loop_messages -%}
    {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) -%}
        {{ raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") }}
    {%- endif -%}
    {%- if (message['role'] == 'assistant') -%}
        {%- set role = "model" -%}
    {%- else -%}
        {%- set role = message['role'] -%}
    {%- endif -%}
    {{ '<start_of_turn>' + role + '
' + (first_user_prefix if loop.first else "") }}
    {%- if message['content'] is string -%}
        {{ message['content'] | trim }}
    {%- elif message['content'] is iterable -%}
        {%- for item in message['content'] -%}
            {%- if item['type'] == 'audio' -%}
                {{ '<audio_soft_token>' }}
            {%- elif item['type'] == 'image' -%}
                {{ '<image_soft_token>' }}
            {%- elif item['type'] == 'text' -%}
                {{ item['text'] | trim }}
            {%- endif -%}
        {%- endfor -%}
    {%- else -%}
        {{ raise_exception("Invalid content type") }}
    {%- endif -%}
    {{ '<end_of_turn>
' }}
{%- endfor -%}
{%- if add_generation_prompt -%}
    {{'<start_of_turn>model
'}}
{%- endif -%}

  tokenizer.ggml.add_bos_token: true        # 是否在序列开始添加BOS token
  tokenizer.ggml.add_eos_token: false       # 是否在序列结束添加EOS token 
  tokenizer.ggml.add_sep_token: false       # 是否添加分隔符token 
  tokenizer.ggml.add_space_prefix: false    # 是否添加空格前缀
  tokenizer.ggml.bos_token_id: 2            # Beginning of Sequence token的ID
  tokenizer.ggml.eos_token_id: 1            # End of Sequence token的ID
  tokenizer.ggml.model: llama               # 使用的分词器模型类型
  tokenizer.ggml.padding_token_id: 0        # Padding token的ID
  tokenizer.ggml.pre: default               # 预处理方式
  tokenizer.ggml.unknown_token_id: 3        # Unknown token的ID

模型基本信息:
  架构: gemma3n
  名字: Gg Hf Gm_Gemma 3n E4B It
  量化类型: 15 (或从文件名推断 Q4_K_M)
  上下文长度: 未知

生成流程

  1. 输入
  2. 阶段 1:前端 / 客户端预处理
    1. 分词(Tokenization) 使用模型自带的 tokenizer(SentencePiece 或类似 Llama 风格的分词器)把你的文字切成 token。 根据元数据:
      • tokenizer.ggml.add_bos_token: true → 会在最前面自动加 BOS token(id=2)
      • add_space_prefix: false → 不会在开头加空格
      • chat_template 会把你的输入包装成对话格式(因为是 instruct 模型)
  1. 阶段 2:模型加载阶段(程序启动时一次性完成)
    1. GGUF 文件被 mmap(内存映射)加载到内存,几乎零拷贝,速度极快。
    2. 根据元数据:
      • 35 层 Transformer block 被加载
      • 所有权重(Q4_K_M 量化后的 6.9B 参数)被解码到内存
      • KV Cache 预分配(根据上下文长度 32768,但实际只用你需要的部分)
      • RoPE 基频设为 1,000,000,支持长序列
      • 注意力机制使用 GQA(8 query heads 共享 2 kv heads)+ 滑动窗口 512 + 共享 KV 层 15 层
  1. 阶段 3:Prefill(预填充)阶段 —— 处理你的输入
    1. 模型一次性处理你全部的输入 token(包括 BOS、<start_of_turn>user ... <end_of_turn> <start_of_turn>model)
    2. 这是计算最密集的部分(所有层都要完整跑一遍)
    3. 输出:最后一个位置的隐藏状态(2048 维向量),以及所有历史 KV Cache(Key 和 Value 缓存)

关键优化点(Gemma 3n 专有):

    1. shared_kv_layers=15 → 中间 15 层的 KV 被复用,节省大量内存和计算
    2. sliding_window=512 → 超过 512 个 token 的历史注意力被截断,只保留最近 512 个,加速长对话
    3. altup / PLE → 每层输入嵌入使用低维缓存(256 维),进一步省内存
  1. 阶段 4:生成阶段(Autoregressive 自回归生成)

这是模型真正“写文章”的部分,一次生成一个 token,循环进行。

循环过程(重复 N 次,直到生成结束):

取上一步的输出 token(第一次是 prefill 最后的隐藏状态)

输入到下一层 Transformer

      • 用 RoPE 编码当前位置(freq_base=1000000)
          1. 注意力计算:GQA + 滑动窗口 + KV 共享
          2. 前馈网络(16384 维中间层)
          3. RMSNorm(epsilon=1e-6)

得到 logits(词汇表大小的概率分布,通常 256000+ 个词汇)

采样下一个 token

      • temperature(默认 0.7–1.0)
          1. top_p / top_k
          2. repetition_penalty
          3. mirostat / DRY 等高级采样
          4. 例如采样到 token id=1234(对应某个词)

把新 token 加到序列末尾,更新 KV Cache

重复 步骤 1–5,直到:

      • 生成到 EOS token(id=1)
      • 达到最大长度(max_tokens)
      • 或手动停
  1. 阶段 5:后处理 & 输出
  • 把生成的 token id 序列送回 tokenizer 解码成文字
  • 去掉 <start_of_turn>model 等特殊标记
  • 返回给用户:
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐