第 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/:digest 与 POST /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 会:
- 修复 blobs 目录中的异常文件。
- 读取 manifests。
- 如果没有
OLLAMA_NOPRUNE,清理不再被引用的 layers。 - 清理 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 文件格式。