跳转至

第 6 章:模型存储——Manifest、Layer、Blob 与 Model

一、为什么模型不是一个文件

用户看到的是 gemma4:latest,运行时需要的却可能是:

模型引用
  └── manifest
       ├── config layer       模型架构、模板、能力、参数
       ├── model layer         GGUF 权重
       ├── adapter layer       LoRA 等增量权重
       ├── projector layer     多模态投影
       └── digest → blob file  实际内容

这种结构带来三个直接收益:去重、可复用、可增量传输。两个模型可以共享同一权重 Blob,只在 config 或 adapter 层不同;pull/push 只需要比较 digest。

二、Manifest 的角色

manifest.Manifest 是一个面向内容寻址的索引,manifest.Layer 描述 digest、size、media type 等信息。manifest/paths.go 则把模型名映射到本地 manifests 目录,把 digest 映射到 blobs 目录。

~/.ollama/models/
├── manifests/...
└── blobs/
    └── sha256-...

实际根目录由 envconfig.Models() 等配置决定,不应在代码里写死用户 home。路径层封装的价值就是让 server、CLI 和 prune 逻辑共享同一规则。

三、Pull 的真实工作不是“下载一个文件”

sequenceDiagram
    participant C as api.Client
    participant S as server.PullHandler
    participant R as library/remote
    participant B as blob store
    participant M as manifest
    C->>S: POST /api/pull name
    S->>R: 查询 manifest
    R-->>S: config + layer digests
    loop 每个 digest
        S->>B: HEAD 本地是否存在
        S->>R: 缺失则分块下载
        R-->>B: 写入 blob + 进度
    end
    S->>M: 写入本地 manifest
    S-->>C: NDJSON progress

HEAD /api/blobs/:digestPOST /api/blobs/:digest 使创建模型时可以先上传唯一 Blob,再把多个 Blob 组装成 manifest。

四、模型名不是路径

types/model.Name 负责解析 registry、namespace、repository、tag/digest 等部分;handler 先验证名字,再将它转换为 manifest 查询路径。这样可以阻止任意路径穿越,也让 foo:latest、带 namespace 的引用和显式 digest 具有统一语义。

读模型相关代码时,应把以下几层区分开:

责任
types/model 名称语法与能力常量
manifest 引用到 layer/digest 的索引
fs / fs/gguf 文件格式读取
server.Model 将 config、权重、模板等组合成可运行模型
llm 将模型交给 native runner

五、server.Model 是“可运行模型视图”

manifest 是存储结构,server.Model 是运行时结构。后者需要知道:

  • 主模型路径
  • projector/adapter 路径
  • config 与 model family
  • template/system/messages/options
  • capabilities
  • parser/renderer
  • remote/cloud 配置

这解释了为什么 GetModel 不只是读一个 JSON:它要解析 manifest layer、读取 GGUF metadata、恢复 Modelfile 语义,最后生成 Scheduler 可消费的对象。

六、Prune 的安全顺序

服务启动时 server.Serve 会:

  1. 修复 blobs 目录中的异常文件。
  2. 读取 manifests。
  3. 如果没有 OLLAMA_NOPRUNE,清理不再被引用的 layers。
  4. 清理 manifests 目录中的冗余路径。

关键是先以 manifest 作为引用事实,再删除 Blob。如果 manifest 损坏,代码会跳过 prune,并提示重新 pull/delete,避免把无法判断的文件当垃圾删掉。

七、Pull/Push 的进度模型

传输 API 使用 NDJSON progress:每一行表示当前 digest、状态、completed、total 或 error。CLI 的 progress 包只需要订阅 ProgressResponse,不需要了解服务端是 HTTP 下载、文件写入还是云端传输。

八、设计取舍

  • 内容寻址换来去重和断点友好,但调试时需要同时追踪名字、manifest、digest 和文件。
  • Manifest 与 Blob 分离换来组合能力,但读取模型必须做更多索引工作。
  • 服务启动时 prune换来长期磁盘卫生,但必须对损坏 manifest 保守处理。
  • Model 作为运行时视图把存储细节隔离掉,使 Scheduler 不必理解 registry 文件格式。