llama.cpp 模型加载机制深度解析
概述
llama.cpp 的模型加载系统是一个高度优化的、支持多后端、多设备的模型权重加载框架。它通过精心设计的数据结构和加载流程,实现了:
- 零拷贝加载:通过内存映射(mmap)实现模型文件的零拷贝加载
- 多设备支持:自动发现和分配GPU、CPU等设备,支持模型分层加载到不同设备
- 架构无关:通过统一的抽象支持Llama、Qwen、DeepSeek、Mamba、RWKV等多种架构
- 高性能I/O:支持直接I/O、异步上传、预取等优化策略
本文将从数据结构、加载流程、I/O机制、设备管理等多个维度深入解析模型加载的实现细节。
关键数据结构
llama_model – 核心模型容器
llama_model 是模型加载后的核心容器,它持有模型的所有权重指针、元数据和设备信息。
结构定义
struct llama_model {
llm_type type = LLM_TYPE_UNKNOWN; // 模型规模(如3B、7B)
llm_arch arch = LLM_ARCH_UNKNOWN; // 模型架构(如Llama、Qwen)
std::string name = "n/a"; // 模型名称
llama_hparams hparams = {}; // 超参数
llama_vocab vocab; // 词表
// 全局权重张量
struct ggml_tensor * tok_embd = nullptr; // Token Embeddings
struct ggml_tensor * output = nullptr; // Output weights
struct ggml_tensor * output_norm = nullptr; // Output normalization
// 分类器相关(用于分类模型)
struct ggml_tensor * cls = nullptr;
// 层权重数组
std::vector<llama_layer> layers;
// GGUF元数据
std::unordered_map<std::string, std::string> gguf_kv;
// 设备列表
std::vector<ggml_backend_dev_t> devices;
// LoRA适配器
std::unordered_set<llama_adapter_lora *> loras;
// 性能统计
int64_t t_load_us = 0;
int64_t t_start_us = 0;
// Pimpl模式隐藏实现细节
private:
struct impl;
std::unique_ptr<impl> pimpl;
};
核心设计点
- 结构化抽象 – layers数组
- std::vector
layers 是核心设计 - 将重复的层权重封装到 llama_layer 中
- 支持动态层数,无需修改结构体定义
- 设备管理
- devices 向量记录所有使用的后端设备
- dev_layer(int il) 返回第 il 层所在的设备
- dev_output() 返回输出层所在的设备
- 支持多GPU分层加载
- Pimpl模式
- 隐藏底层实现细节(mmap管理、内存映射句柄等)
- 保持API稳定性,修改实现不影响头文件
- 权重指针而非数据
- llama_model 不存储实际权重数据
- 只存储指向权重数据的指针(ggml_tensor *)
- 实际数据通过mmap映射或buffer管理
关键方法
- load_arch(): 从loader加载架构信息
- load_hparams(): 加载超参数
- load_vocab(): 加载词表
- load_tensors(): 创建张量并分配设备
- build_graph(): 构建计算图
- create_memory(): 创建KV Cache内存
llama_layer – 单层权重抽象
llama_layer 封装了Transformer单层的所有权重张量,支持多种架构变体。
结构定义(部分)
struct llama_layer {
// 归一化层
struct ggml_tensor * attn_norm = nullptr;
struct ggml_tensor * attn_norm_b = nullptr;
struct ggml_tensor * attn_q_norm = nullptr; // Gemma 2
struct ggml_tensor * attn_k_norm = nullptr;
struct ggml_tensor * ssm_norm = nullptr; // Mamba
// 注意力机制
struct ggml_tensor * wq = nullptr; // Query投影
struct ggml_tensor * wk = nullptr; // Key投影
struct ggml_tensor * wv = nullptr; // Value投影
struct ggml_tensor * wo = nullptr; // Output投影
struct ggml_tensor * wqkv = nullptr; // 合并QKV(某些架构)
// 注意力偏置
struct ggml_tensor * bq = nullptr;
struct ggml_tensor * bk = nullptr;
struct ggml_tensor * bv = nullptr;
struct ggml_tensor * bo = nullptr;
// Feed-Forward网络
struct ggml_tensor * ffn_norm = nullptr;
struct ggml_tensor * ffn_gate = nullptr;
struct ggml_tensor * ffn_up = nullptr;
struct ggml_tensor * ffn_down = nullptr;
// 混合专家系统(MoE)
struct ggml_tensor * ffn_gate_inp = nullptr; // 门控输入
struct ggml_tensor * ffn_gate_exps = nullptr; // 专家门控权重
struct ggml_tensor * ffn_up_exps = nullptr; // 专家上投影
struct ggml_tensor * ffn_down_exps = nullptr; // 专家下投影
// RoPE相关
struct ggml_tensor * rope_freqs = nullptr;
struct ggml_tensor * rope_long = nullptr; // LongRoPE
struct ggml_tensor * rope_short = nullptr;
// ... 更多架构特定张量
};
设计特点
- 通用层抽象
- 合并了对Gemma、Mixtral、Mamba、RWKV、DeepSeek等模型的支持
- 通过指针是否为nullptr判断架构是否使用该张量
- 架构变体支持
- 归一化变体:支持RMSNorm、LayerNorm、Gemma 2的特殊归一化
- 注意力变体:标准QKV、合并QKV、分组查询注意力(GQA)
- MoE支持:专家门控、共享专家(Shared Experts)
- 非Transformer:Mamba的SSM层、RWKV的循环层
- 加载流程
- 预分配:model.layers.resize(n_layer) 创建N个空层
- 点对点赋值:loader解析GGUF,将张量地址赋给对应层的指针
- 后端绑定:根据设备分配策略,将指针指向CPU或GPU内存
llama_model_loader – 权重加载器
llama_model_loader 是模型加载的”施工队”,负责GGUF文件解析、权重索引、内存映射和数据搬运。
核心成员
struct llama_model_loader {
// GGUF元数据
gguf_context_ptr meta; // GGUF解析器上下文
// 文件管理
llama_files files; // 主文件+分片文件句柄
llama_mmaps mappings; // 内存映射句柄
// 权重索引表(核心数据结构)
std::map<std::string, llama_tensor_weight, weight_name_comparer> weights_map;
// KV覆盖
std::unordered_map<std::string, llama_model_kv_override> kv_overrides;
// 张量Buffer类型覆盖
const llama_model_tensor_buft_override * tensor_buft_overrides;
// I/O配置
bool use_mmap;
bool use_direct_io;
bool check_tensors;
};
权重索引表:weights_map
这是loader最核心的数据结构,充当”权重目录”:
// Key: tensor名称,如 "tok_embeddings.weight", "blk.0.attn_q.weight"
// Value: llama_tensor_weight(定位信息)
struct llama_tensor_weight {
uint16_t idx; // 文件分片索引(主文件=0,split可能是1、2...)
size_t offs; // 在文件中的字节偏移
ggml_tensor * tensor; // 张量元数据(shape/type/name)
};
weight_name_comparer:自定义排序规则
- 解析层索引(如 blk.5 → 5)
- 按层号排序,而非纯字符串排序
- 优化磁盘读取顺序,利用预读缓存
关键流程
- 构造函数:初始化GGUF解析
llama_model_loader::llama_model_loader(...) { // 1. 调用gguf_init_from_file解析文件头 meta.reset(gguf_init_from_file(fname.c_str(), params)); // 2. 识别架构 get_key(LLM_KV_GENERAL_ARCHITECTURE, arch_name, false); llm_kv = LLM_KV(llm_arch_from_string(arch_name)); // 3. 打开文件句柄 files.emplace_back(new llama_file(fname.c_str(), "rb", use_direct_io)); // 4. 构建weights_map索引表 for (ggml_tensor * cur = ggml_get_first_tensor(ctx); cur; ...) { weights_map.emplace(tensor_name, llama_tensor_weight(...)); } // 5. 处理分片文件(如果有) // ... } - init_mappings:初始化内存映射
void llama_model_loader::init_mappings(bool prefetch, llama_mlocks * mlock_mmaps) { if (use_mmap) { for (const auto & file : files) { // 创建mmap映射 std::unique_ptr<llama_mmap> mapping = std::make_unique<llama_mmap>(file.get(), prefetch ? -1 : 0, is_numa); mappings.emplace_back(std::move(mapping)); // 可选:内存锁定 if (mlock_mmaps) { std::unique_ptr<llama_mlock> mlock_mmap(new llama_mlock()); mlock_mmap->init(mapping->addr()); mlock_mmaps->emplace_back(std::move(mlock_mmap)); } } } } - create_tensor:创建张量对象(空壳)
- 从weights_map查找元数据
- 在指定的ggml_context中创建tensor对象
- 此时tensor->data仍为nullptr
- load_all_data:加载实际数据
- 遍历所有tensor
- 根据use_mmap选择路径:
- mmap路径:ggml_backend_tensor_alloc(buf_mmap, cur, data) 绑定mmap地址
- 文件读取路径:file->read_raw(cur->data, n_size) 读取到内存
- GPU异步上传:分块读取+异步上传到GPU
llama_hparams – 超参数蓝图
llama_hparams 存储模型的数学参数,决定计算图的形状和逻辑。
核心参数分类
- 基础架构参数
- n_embd: 隐藏层维度(如4096)
- n_layer: 总层数
- n_ctx_train: 训练时上下文长度
- n_head_arr[]: 每层注意力头数(数组支持每层不同)
- n_head_kv_arr[]: 每层KV头数(GQA支持)
- 注意力机制变体
- n_embd_head_k_mla: MLA压缩后的KV维度(DeepSeek-V2/V3)
- n_swa: 滑动窗口大小(Mistral)
- swa_layers[]: 哪些层启用滑动窗口
- 位置编码(RoPE & YaRN)
- rope_freq_base_train: RoPE基础频率
- rope_freq_scale_train: RoPE缩放因子
- rope_scaling_type_train: 缩放类型(linear/yarn/longrope)
- yarn_ext_factor: YaRN扩展因子
- rope_sections[]: 部分维度旋转(Partial RoPE)
- 混合专家模型(MoE)
- n_expert: 总专家数
- n_expert_used: 每个token激活的专家数(如8选2)
- n_ff_exp: 专家FFN维度
- n_ff_shexp: 共享专家维度(DeepSeek)
- expert_gating_func: 门控函数类型
- 状态空间模型(SSM/Mamba)
- ssm_d_conv: 卷积维度
- ssm_d_inner: 内部维度
- ssm_d_state: 状态维度
- recurrent_layer_arr[]: 哪些层是循环层(混合架构)
- 归一化与数值稳定性
- f_norm_rms_eps: RMSNorm的epsilon
- f_attn_logit_softcapping: Gemma 2的注意力分数软上限
- f_residual_scale: 残差连接缩放(Granite)
加载流程
void llama_model::load_hparams(llama_model_loader & ml) {
const gguf_context * ctx = ml.meta.get();
// 1. 保存所有KV对到gguf_kv(用于元数据查询)
for (int i = 0; i < gguf_get_n_kv(ctx); i++) {
const char * name = gguf_get_key(ctx, i);
const std::string value = gguf_kv_to_str(ctx, i);
gguf_kv.emplace(name, value);
}
// 2. 读取基础参数
ml.get_key(LLM_KV_CONTEXT_LENGTH, hparams.n_ctx_train);
ml.get_key(LLM_KV_EMBEDDING_LENGTH, hparams.n_embd);
ml.get_key(LLM_KV_BLOCK_COUNT, hparams.n_layer);
// 3. 读取数组参数(支持每层不同)
ml.get_key_or_arr(LLM_KV_FEED_FORWARD_LENGTH, hparams.n_ff_arr, hparams.n_layer, false);
ml.get_key_or_arr(LLM_KV_ATTENTION_HEAD_COUNT, hparams.n_head_arr, hparams.n_layer, false);
// 4. 架构特定参数(根据arch分支)
switch (arch) {
case LLM_ARCH_LLAMA: /* ... */ break;
case LLM_ARCH_DEEPSEEK: /* ... */ break;
// ...
}
// 5. 参数校验与推导
// 如果某些参数缺失,根据架构进行推导
}
为什么hparams要独立出来?
- 多版本适配:不同架构可能完全没有某些参数(如Mamba没有n_head),但都有n_embd
- KV Cache管理:hparams决定KV Cache的内存布局和大小
- 计算图构建:推理时算子需要从hparams读取数学常数
GGML相关结构
gguf_context
GGUF文件的内存映射索引,存储所有KV对和张量元信息。
struct gguf_context {
uint32_t version; // GGUF协议版本
// KV键值对存储
std::vector<gguf_kv> kv; // 所有元数据KV对
// 张量信息
std::vector<gguf_tensor_info> info; // 所有权重的"档案"
// 物理布局
size_t alignment;
size_t offset; // 数据区偏移
void * data; // 数据区指针(如果已映射)
};
ggml_backend_buffer
张量真实的”物理房产”,解决跨设备内存管理问题。
struct ggml_backend_buffer {
struct ggml_backend_buffer_i iface; // 函数指针接口
ggml_backend_buffer_type_t buft; // Buffer类型(CPU/CUDA/Metal等)
void * context; // 后端内部状态
size_t size; // 大小
enum ggml_backend_buffer_usage usage; // 用途
};
关键设计:
- iface 定义了如何操作这块内存(分配、释放、拷贝等)
- buft 标识内存由哪个后端分配
- 支持CPU、CUDA、Metal、Vulkan等多种后端
模型加载完整流程
入口函数:llama_model_load_from_file_impl
这是模型加载的入口点,负责设备发现、模型对象创建和错误处理。
static struct llama_model * llama_model_load_from_file_impl(
const std::string & path_model,
std::vector<std::string> & splits,
struct llama_model_params params) {
// 1. 初始化时间统计
ggml_time_init();
// 2. 检查后端是否已加载
if (!params.vocab_only && ggml_backend_reg_count() == 0) {
LLAMA_LOG_ERROR("no backends are loaded\n");
return nullptr;
}
// 3. 设置默认进度回调(如果未提供)
if (params.progress_callback == NULL) {
params.progress_callback = [](float progress, void * ctx) {
// 打印进度点
return true;
};
}
// 4. 创建模型对象
llama_model * model = new llama_model(params);
// 5. 设备发现与选择(详见"设备管理"章节)
if (params.devices) {
// 使用用户指定的设备
for (ggml_backend_dev_t * dev = params.devices; *dev; ++dev) {
model->devices.push_back(*dev);
}
} else {
// 自动发现设备
// - 收集所有GPU设备
// - 收集集成GPU(iGPU)
// - 收集RPC服务器
// - 按优先级排序
}
// 6. 单GPU模式处理
if (params.split_mode == LLAMA_SPLIT_MODE_NONE) {
if (params.main_gpu < 0) {
model->devices.clear(); // 只用CPU
} else {
// 只保留指定的GPU
model->devices = {model->devices[params.main_gpu]};
}
}
// 7. 打印设备信息
for (auto * dev : model->devices) {
ggml_backend_dev_props props;
ggml_backend_dev_get_props(dev, &props);
LLAMA_LOG_INFO("using device %s - %zu MiB free\n",
ggml_backend_dev_name(dev), props.memory_free/1024/1024);
}
// 8. 调用核心加载函数
const int status = llama_model_load(path_model, splits, *model, params);
// 9. 错误处理
if (status < 0) {
if (status == -1) {
LLAMA_LOG_ERROR("failed to load model\n");
} else if (status == -2) {
LLAMA_LOG_INFO("cancelled model load\n");
}
llama_model_free(model);
return nullptr;
}
return model;
}
核心加载函数:llama_model_load
这是模型加载的核心流程,按顺序执行各个加载阶段。
static int llama_model_load(
const std::string & fname,
std::vector<std::string> & splits,
llama_model & model,
llama_model_params & params) {
// 初始化时间统计
model.t_load_us = 0;
time_meas tm(model.t_load_us);
model.t_start_us = tm.t_start_us;
try {
// ========== 阶段0:创建Loader ==========
llama_model_loader ml(fname, splits,
params.use_mmap, params.use_direct_io,
params.check_tensors, params.no_alloc,
params.kv_overrides, params.tensor_buft_overrides);
ml.print_info(); // 打印模型基本信息
model.hparams.vocab_only = params.vocab_only;
model.hparams.no_alloc = params.no_alloc;
// ========== 阶段1:架构识别 ==========
try {
model.load_arch(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model architecture: " +
std::string(e.what()));
}
// ========== 阶段2:超参数加载 ==========
try {
model.load_hparams(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model hyperparameters: " +
std::string(e.what()));
}
// CLIP模型特殊处理
if (model.arch == LLM_ARCH_CLIP) {
throw std::runtime_error("CLIP cannot be used as main model");
}
// ========== 阶段3:词表加载 ==========
try {
model.load_vocab(ml);
} catch(const std::exception & e) {
throw std::runtime_error("error loading model vocabulary: " +
std::string(e.what()));
}
// ========== 阶段4:统计信息 ==========
model.load_stats(ml);
model.print_info(); // 打印完整模型信息
// ========== 早期退出:仅词表模式 ==========
if (params.vocab_only) {
LLAMA_LOG_INFO("vocab only - skipping tensors\n");
return 0;
}
// ========== 阶段5:张量创建与加载 ==========
if (!model.load_tensors(ml)) {
return -2; // 被进度回调取消
}
} catch (const std::exception & err) {
LLAMA_LOG_ERROR("error loading model: %s\n", err.what());
return -1;
}
return 0;
}
阶段一:架构识别与元数据解析
load_arch实现
void llama_model::load_arch(llama_model_loader & ml) {
arch = ml.get_arch(); // 从loader获取架构枚举
if (arch == LLM_ARCH_UNKNOWN) {
throw std::runtime_error("unknown model architecture: '" +
ml.get_arch_name() + "'");
}
}
loader中的架构识别
// llama_model_loader构造函数中
llama_model_loader::llama_model_loader(...) {
// 1. 解析GGUF文件头
meta.reset(gguf_init_from_file(fname.c_str(), params));
// 2. 读取架构名称
std::string arch_name;
get_key(llm_kv(LLM_KV_GENERAL_ARCHITECTURE), arch_name, false);
// 3. 转换为枚举
llm_kv = LLM_KV(llm_arch_from_string(arch_name));
// llm_arch_from_string("llama") -> LLM_ARCH_LLAMA
}
架构识别流程:
- GGUF文件头包含 general.architecture KV对
- 读取字符串值(如”llama”、”qwen”、”deepseek”)
- 通过 llm_arch_from_string() 转换为枚举
- 如果无法识别,抛出异常
阶段二:超参数加载
详见 llama_hparams – 超参数蓝图 章节。
关键点:
- 从GGUF的KV对中读取所有超参数
- 支持数组参数(每层不同)
- 架构特定参数根据arch分支处理
- 参数校验与默认值推导
阶段三:词表加载
void llama_model::load_vocab(llama_model_loader & ml) {
const auto kv = LLM_KV(arch); // 获取架构特定的KV键前缀
vocab.load(ml, kv); // 加载词表
}
词表加载包括:
- Token ID到字符串的映射
- Token类型(普通/BOS/EOS/等)
- 特殊Token(如<|im_start|>)
- 子词合并规则(BPE/SPM等)
阶段四:张量创建与设备分配
这是最复杂的阶段,涉及张量创建、设备分配、Buffer类型选择等。
load_tensors流程概览
bool llama_model::load_tensors(llama_model_loader & ml) {
const int n_layer = hparams.n_layer;
const int n_gpu_layers = this->n_gpu_layers();
// ========== 步骤1:构建Buffer类型列表 ==========
pimpl->cpu_buft_list = make_cpu_buft_list(devices, ...);
for (auto * dev : devices) {
buft_list_t buft_list = make_gpu_buft_list(dev, split_mode, tensor_split);
buft_list.insert(buft_list.end(),
pimpl->cpu_buft_list.begin(), pimpl->cpu_buft_list.end());
pimpl->gpu_buft_list.emplace(dev, std::move(buft_list));
}
// ========== 步骤2:计算设备分割点 ==========
std::vector<float> splits(n_devices());
// 根据tensor_split参数或默认策略(按显存)分配
// ========== 步骤3:分配层到设备 ==========
auto get_layer_buft_list = [&](int il) -> layer_dev {
if (il < i_gpu_start || (il - i_gpu_start) >= act_gpu_layers) {
return {cpu_dev, &pimpl->cpu_buft_list};
}
const int layer_gpu = /* 根据splits计算 */;
return {devices.at(layer_gpu), &pimpl->gpu_buft_list.at(devices.at(layer_gpu))};
};
pimpl->dev_input = {cpu_dev, &pimpl->cpu_buft_list};
pimpl->dev_layer.resize(n_layer);
for (int il = 0; il < n_layer; ++il) {
pimpl->dev_layer[il] = get_layer_buft_list(il);
}
pimpl->dev_output = get_layer_buft_list(n_layer);
// ========== 步骤4:创建GGML Context ==========
// 为每个Buffer类型创建独立的context
std::map<ggml_backend_buffer_type_t, ggml_context_ptr> ctx_map;
auto ctx_for_buft = [&](ggml_backend_buffer_type_t buft) -> ggml_context * {
// 查找或创建context
};
// ========== 步骤5:创建张量对象(架构特定) ==========
layers.resize(n_layer);
auto create_tensor = [&](const LLM_TN_IMPL & tn,
const std::initializer_list<int64_t> & ne,
int flags) -> ggml_tensor * {
// 1. 从weights_map查找元数据
ggml_tensor * t_meta = ml.get_tensor_meta(tn.str().c_str());
// 2. 选择Buffer类型
buft_list_t * buft_list = /* 根据tensor层级选择 */;
ggml_backend_buffer_type_t buft = select_weight_buft(hparams, t_meta, op, *buft_list);
// 3. 检查覆盖规则
if (ml.tensor_buft_overrides) {
// 应用用户指定的覆盖
}
// 4. 获取或创建context
ggml_context * ctx = ctx_for_buft(buft);
// 5. 创建tensor对象(此时data仍为nullptr)
return ml.create_tensor(ctx, tn, ne, flags);
};
// 架构特定的张量创建
switch (arch) {
case LLM_ARCH_LLAMA:
tok_embd = create_tensor(tn(LLM_TENSOR_TOKEN_EMBD, "weight"),
{n_embd, n_vocab}, 0);
output_norm = create_tensor(tn(LLM_TENSOR_OUTPUT_NORM, "weight"),
{n_embd}, 0);
output = create_tensor(tn(LLM_TENSOR_OUTPUT, "weight"),
{n_embd, n_vocab}, TENSOR_NOT_REQUIRED);
for (int i = 0; i < n_layer; ++i) {
auto & layer = layers[i];
layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", i),
{n_embd}, 0);
layer.wq = create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", i),
{n_embd, n_embd_head_k * n_head}, 0);
// ... 更多张量
}
break;
// ... 其他架构
}
// ========== 步骤6:初始化内存映射 ==========
ml.init_mappings(params.prefetch, &pimpl->mlock_mmaps);
// ========== 步骤7:加载实际数据 ==========
if (!ml.load_all_data(ctxs_bufs, params.progress_callback,
params.progress_callback_user_data)) {
return false; // 被取消
}
// ========== 步骤8:保存Context和Buffer ==========
for (auto & [buft, ctx] : ctx_map) {
std::vector<ggml_backend_buffer_ptr> bufs;
// 收集该context下的所有buffer
pimpl->ctxs_bufs.emplace_back(ctx, std::move(bufs));
}
return true;
}
设备分配策略
- 输入层:始终在CPU(dev_input = cpu_dev)
- 中间层:根据n_gpu_layers和tensor_split分配
- 输出层:与最后一层相同设备
分割计算:
// 默认分割:按显存比例
if (all_zero) {
for (size_t i = 0; i < n_devices(); ++i) {
ggml_backend_dev_props props;
ggml_backend_dev_get_props(devices[i], &props);
total_free += props.memory_free;
}
float acc = 0.0f;
for (size_t i = 0; i < n_devices(); ++i) {
ggml_backend_dev_props props;
ggml_backend_dev_get_props(devices[i], &props);
acc += props.memory_free / total_free;
splits[i] = acc;
}
}
阶段五:权重数据加载
load_all_data实现
bool llama_model_loader::load_all_data(
std::vector<std::pair<ggml_context_ptr, std::vector<ggml_backend_buffer_ptr>>> & ctxs_bufs,
llama_progress_callback progress_callback,
void * progress_callback_user_data) {
// ========== 准备异步上传(如果支持) ==========
ggml_backend_t upload_backend = /* 检查是否支持异步上传 */;
std::vector<ggml_backend_buffer_ptr> host_buffers;
std::vector<void *> host_ptrs;
std::vector<ggml_backend_event_ptr> events;
// ========== 遍历所有tensor ==========
for (struct ggml_tensor * cur = ggml_get_first_tensor(ctx);
cur != NULL;
cur = ggml_get_next_tensor(ctx, cur)) {
const auto * weight = get_weight(ggml_get_name(cur));
if (weight == nullptr) continue;
// 进度回调
if (progress_callback) {
if (!progress_callback((float) size_done / size_data,
progress_callback_user_data)) {
return false; // 用户取消
}
}
size_t n_size = ggml_nbytes(cur);
// ========== 路径A:mmap模式(零拷贝) ==========
if (use_mmap) {
const auto & mapping = mappings.at(weight->idx);
uint8_t * data = (uint8_t *) mapping->addr() + weight->offs;
// 验证数据(如果启用)
if (check_tensors) {
validation_result.emplace_back(std::async(std::launch::async,
[cur, data, n_size] {
return std::make_pair(cur,
ggml_validate_row_data(cur->type, data, n_size));
}));
}
// 绑定到mmap内存
ggml_backend_buffer_t buf_mmap = bufs.at(weight->idx);
if (buf_mmap && cur->data == nullptr) {
ggml_backend_tensor_alloc(buf_mmap, cur, data);
// tensor->buffer = buf_mmap
// tensor->data = data (指向mmap地址)
} else {
// 拷贝数据(如果tensor已有buffer)
ggml_backend_tensor_set(cur, data, 0, n_size);
}
}
// ========== 路径B:文件读取模式 ==========
else {
const auto & file = files.at(weight->idx);
// CPU buffer:直接读取
if (ggml_backend_buffer_is_host(cur->buffer)) {
file->seek(weight->offs, SEEK_SET);
file->read_raw(cur->data, n_size);
if (check_tensors) {
validation_result.emplace_back(std::async(std::launch::async,
[cur, n_size] {
return std::make_pair(cur,
ggml_validate_row_data(cur->type, cur->data, n_size));
}));
}
}
// GPU buffer:异步上传
else {
if (upload_backend) {
// 分块读取+异步上传
size_t offset = weight->offs;
size_t aligned_offset = offset & ~(alignment - 1);
// ... 对齐读取边界
while (bytes_read < read_end - read_start) {
// 等待上一个上传完成
ggml_backend_event_synchronize(events[buffer_idx]);
// 读取对齐块
file->read_raw_unsafe(host_ptrs[buffer_idx], read_size);
// 异步上传到GPU
ggml_backend_tensor_set_async(upload_backend, cur,
host_ptrs[buffer_idx], data_read, data_to_copy);
ggml_backend_event_record(events[buffer_idx], upload_backend);
buffer_idx = (buffer_idx + 1) % n_buffers; // 轮转缓冲区
}
} else {
// 同步上传
read_buf.resize(n_size);
file->seek(weight->offs, SEEK_SET);
file->read_raw(read_buf.data(), n_size);
ggml_backend_tensor_set(cur, read_buf.data(), 0, n_size);
}
}
}
size_done += n_size;
}
// ========== 验证结果 ==========
if (check_tensors) {
for (auto & future : validation_result) {
auto [tensor, valid] = future.get();
if (!valid) {
throw std::runtime_error(format("tensor '%s' has invalid data",
ggml_get_name(tensor)));
}
}
}
return true;
}
三种数据加载路径对比
| 路径 | 适用场景 | 性能 | 内存占用 |
|---|---|---|---|
| mmap绑定 | CPU推理,mmap可用 | 最快(零拷贝) | 最小(共享文件页) |
| 文件读取+CPU | CPU推理,mmap不可用 | 中等 | 中等(需要内存拷贝) |
| 异步上传 | GPU推理,支持异步 | 快(重叠I/O和计算) | 中等(需要pinned memory) |
| 同步上传 | GPU推理,不支持异步 | 慢 | 中等 |
GGUF协议与I/O抽象
GGUF文件格式解析
GGUF(GPT-Generated Unified Format)是llama.cpp使用的二进制模型格式。
文件结构
┌─────────────────────────────────────┐
│ Magic Number (4 bytes) │ "GGUF"
├─────────────────────────────────────┤
│ Version (4 bytes) │ 协议版本
├─────────────────────────────────────┤
│ n_kv (8 bytes) │ KV对数量
├─────────────────────────────────────┤
│ KV Pairs (变长) │
│ - Key: 字符串长度 + 字符串 │
│ - Type: 类型枚举 │
│ - Value: 根据类型存储 │
├─────────────────────────────────────┤
│ n_tensors (8 bytes) │ 张量数量
├─────────────────────────────────────┤
│ Tensor Info Array (变长) │
│ - Name: 字符串长度 + 字符串 │
│ - n_dims: 维度数 │
│ - dims[]: 各维度大小 │
│ - type: 数据类型 │
│ - offset: 数据偏移(相对数据区) │
├─────────────────────────────────────┤
│ Alignment Padding │ 对齐到16字节
├─────────────────────────────────────┤
│ Tensor Data (变长) │ 实际权重数据
└─────────────────────────────────────┘
解析流程
struct gguf_context * gguf_init_from_file_impl(FILE * file, ...) {
const struct gguf_reader gr(file);
struct gguf_context * ctx = new gguf_context;
// 1. 校验Magic Number
uint32_t magic = gr.read_u32();
if (magic != GGUF_MAGIC) {
throw std::runtime_error("invalid GGUF magic");
}
// 2. 读取版本
ctx->version = gr.read_u32();
// 3. 读取KV对数量
int64_t n_kv = gr.read_u64();
// 4. 循环读取所有KV对
for (int64_t i = 0; i < n_kv; ++i) {
std::string key = gr.read_string();
gguf_type type = (gguf_type)gr.read_u32();
switch (type) {
case GGUF_TYPE_UINT8:
ctx->kv.emplace_back(key, gr.read_u8());
break;
case GGUF_TYPE_INT32:
ctx->kv.emplace_back(key, gr.read_i32());
break;
case GGUF_TYPE_FLOAT32:
ctx->kv.emplace_back(key, gr.read_f32());
break;
case GGUF_TYPE_STRING:
ctx->kv.emplace_back(key, gr.read_string());
break;
// ... 更多类型
}
}
// 5. 读取张量数量
int64_t n_tensors = gr.read_u64();
// 6. 循环读取所有张量信息
for (int64_t i = 0; i < n_tensors; ++i) {
struct gguf_tensor_info info;
info.name = gr.read_string();
info.n_dims = gr.read_u32();
for (uint32_t j = 0; j < info.n_dims; ++j) {
info.dims[j] = gr.read_u64();
}
info.type = (ggml_type)gr.read_u32();
info.offset = gr.read_u64(); // 相对数据区的偏移
ctx->info.push_back(info);
}
// 7. 计算数据区偏移
ctx->offset = gr.tell(); // 当前位置就是数据区开始
return ctx;
}
内存映射(mmap)机制
mmap是llama.cpp实现零拷贝加载的核心技术。
mmap初始化
llama_mmap::llama_mmap(struct llama_file * file, size_t prefetch, bool numa) {
size = file->size();
int fd = file->file_id();
#ifdef _POSIX_MAPPED_FILES
int flags = MAP_SHARED;
// Linux优化:预读建议
if (posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL)) {
LLAMA_LOG_WARN("posix_fadvise failed\n");
}
// 预填充页面(如果启用)
if (prefetch) {
flags |= MAP_POPULATE;
}
// 执行mmap
addr = mmap(NULL, file->size(), PROT_READ, flags, fd, 0);
if (addr == MAP_FAILED) {
throw std::runtime_error(format("mmap failed: %s", strerror(errno)));
}
// 预取建议(如果启用)
if (prefetch > 0) {
posix_madvise(addr, std::min(file->size(), prefetch), POSIX_MADV_WILLNEED);
}
// NUMA优化
if (numa) {
posix_madvise(addr, file->size(), POSIX_MADV_RANDOM);
}
#endif
}
mmap优势
- 零拷贝:文件数据直接映射到虚拟地址空间,无需拷贝
- 延迟加载:页面在首次访问时才从磁盘读取(按需分页)
- 内存共享:多个进程可以共享同一份映射,节省内存
- 操作系统优化:OS可以预读、缓存页面
mmap绑定tensor
// 在load_all_data中
if (use_mmap) {
const auto & mapping = mappings.at(weight->idx);
uint8_t * data = (uint8_t *) mapping->addr() + weight->offs;
// 关键:将tensor的data指针直接指向mmap地址
ggml_backend_tensor_alloc(buf_mmap, cur, data);
// 执行后:
// cur->buffer = buf_mmap
// cur->data = data (指向mmap虚拟地址)
}
注意:mmap地址是虚拟地址,实际数据仍在文件页中,由OS管理。
权重索引表构建
weights_map构建流程
llama_model_loader::llama_model_loader(...) {
// 1. 解析GGUF元数据
meta.reset(gguf_init_from_file(fname.c_str(), params));
ggml_context * ctx = contexts[0];
// 2. 遍历GGML context中的所有tensor元数据
for (ggml_tensor * cur = ggml_get_first_tensor(ctx);
cur;
cur = ggml_get_next_tensor(ctx, cur)) {
std::string tensor_name = std::string(cur->name);
// 3. 计算文件偏移
size_t offs = gguf_get_data_offset(gguf_ctx) +
gguf_get_tensor_offset(gguf_ctx, tensor_idx);
// 4. 插入索引表(使用自定义排序)
weights_map.emplace(tensor_name,
llama_tensor_weight(files[0].get(), 0, meta.get(), cur));
}
// 5. 处理分片文件(如果有)
for (const auto & split : splits) {
// 解析分片GGUF
// 合并tensor到weights_map(idx递增)
}
}
weight_name_comparer排序规则
struct weight_name_comparer {
bool operator()(const std::string & a, const std::string & b) const {
int bid_a = -1, bid_b = -1;
// 解析层号(如 "blk.5.attn_q.weight" -> 5)
sscanf(a.c_str(), "blk.%d.", &bid_a);
sscanf(b.c_str(), "blk.%d.", &bid_b);
// 按层号排序
if (bid_a != bid_b) {
return bid_a < bid_b;
}
// 同层按名称排序
return a < b;
}
};
排序目的:优化磁盘读取顺序,利用OS预读缓存。
数据加载路径
详见 阶段五:权重数据加载 章节。
设备管理与多GPU支持
设备发现与选择
自动设备发现
// 在llama_model_load_from_file_impl中
std::vector<ggml_backend_dev_t> gpus;
std::vector<ggml_backend_dev_t> igpus; // 集成GPU
std::vector<ggml_backend_dev_t> rpc_servers; // RPC服务器
for (size_t i = 0; i < ggml_backend_dev_count(); ++i) {
ggml_backend_dev_t dev = ggml_backend_dev_get(i);
switch (ggml_backend_dev_type(dev)) {
case GGML_BACKEND_DEVICE_TYPE_GPU: {
// 检查设备ID是否重复
ggml_backend_dev_props props;
ggml_backend_dev_get_props(dev, &props);
auto it = std::find_if(gpus.begin(), gpus.end(),
[&props](ggml_backend_dev_t d) {
// 比较device_id
return /* 相同ID */;
});
if (it == gpus.end()) {
gpus.push_back(dev);
}
break;
}
case GGML_BACKEND_DEVICE_TYPE_IGPU:
igpus.push_back(dev);
break;
}
}
// 优先级:RPC服务器 > 独立GPU > 集成GPU(仅当无其他设备时)
model->devices.insert(model->devices.begin(),
rpc_servers.begin(), rpc_servers.end());
model->devices.insert(model->devices.end(),
gpus.begin(), gpus.end());
if (model->devices.empty()) {
model->devices.insert(model->devices.end(),
igpus.begin(), igpus.end());
}
单GPU模式
if (params.split_mode == LLAMA_SPLIT_MODE_NONE) {
if (params.main_gpu < 0) {
model->devices.clear(); // 只用CPU
} else {
// 只保留指定的GPU
ggml_backend_dev_t main_gpu = model->devices[params.main_gpu];
model->devices.clear();
model->devices.push_back(main_gpu);
}
}
层到设备的映射策略
分割点计算
// 默认:按显存比例
std::vector<float> splits(n_devices());
if (all_zero) { // tensor_split全为0
size_t total_free = 0;
for (size_t i = 0; i < n_devices(); ++i) {
ggml_backend_dev_props props;
ggml_backend_dev_get_props(devices[i], &props);
total_free += props.memory_free;
}
float acc = 0.0f;
for (size_t i = 0; i < n_devices(); ++i) {
ggml_backend_dev_props props;
ggml_backend_dev_get_props(devices[i], &props);
acc += props.memory_free / total_free;
splits[i] = acc; // 累积比例
}
} else {
// 使用用户指定的tensor_split
float acc = 0.0f;
for (size_t i = 0; i < n_devices(); ++i) {
acc += tensor_split[i];
splits[i] = acc;
}
}
层分配函数
auto get_layer_buft_list = [&](int il) -> layer_dev {
const bool is_swa = il < int(hparams.n_layer) && hparams.is_swa(il);
// CPU层
if (il < i_gpu_start || (il - i_gpu_start) >= act_gpu_layers) {
return {cpu_dev, &pimpl->cpu_buft_list};
}
// GPU层:根据splits查找
float layer_ratio = float(il - i_gpu_start) / act_gpu_layers;
const int layer_gpu = std::upper_bound(splits.begin(),
splits.begin() + n_devices(), layer_ratio) - splits.begin();
auto * dev = devices.at(layer_gpu);
return {dev, &pimpl->gpu_buft_list.at(dev)};
};
示例:3个GPU,splits = [0.33, 0.67, 1.0]
- 层0-10 → GPU 0
- 层11-21 → GPU 1
- 层22-32 → GPU 2
Buffer类型选择
Buffer类型列表构建
// CPU Buffer类型列表
buft_list_t make_cpu_buft_list(...) {
buft_list_t buft_list;
// 1. Split buffer(如果启用)
if (tensor_split && n_devices() > 1) {
// 添加split buffer类型
}
// 2. 默认CPU buffer
buft_list.emplace_back(cpu_dev, ggml_backend_dev_buffer_type(cpu_dev));
// 3. Extra buffer类型(如果启用)
if (use_extra_bufts) {
auto * extra_bufts = ggml_backend_dev_get_extra_bufts(cpu_dev);
// 添加extra buffer
}
return buft_list;
}
// GPU Buffer类型列表
buft_list_t make_gpu_buft_list(ggml_backend_dev_t dev, ...) {
buft_list_t buft_list;
// 1. Split buffer(如果启用)
if (split_mode == LLAMA_SPLIT_MODE_LAYER) {
// 添加split buffer
}
// 2. 默认GPU buffer
buft_list.emplace_back(dev, ggml_backend_dev_buffer_type(dev));
// 3. Host buffer(用于CPU-GPU传输)
auto * host_buft = ggml_backend_dev_host_buffer_type(dev);
if (host_buft) {
buft_list.emplace_back(dev, host_buft);
}
// 4. CPU buffer作为fallback
buft_list.insert(buft_list.end(),
cpu_buft_list.begin(), cpu_buft_list.end());
return buft_list;
}
权重Buffer类型选择
ggml_backend_buffer_type_t select_weight_buft(
const llama_hparams & hparams,
ggml_tensor * w,
ggml_op op,
const buft_list_t & buft_list) {
// 1. 检查权重是否支持该buffer类型
for (const auto & [dev, buft] : buft_list) {
if (weight_buft_supported(hparams, w, op, buft, dev)) {
return buft;
}
}
// 2. 回退到CPU
return ggml_backend_cpu_buffer_type();
}
选择策略:
- 优先选择GPU buffer(如果支持该操作)
- 回退到Host buffer(用于传输)
- 最后回退到CPU buffer
异步上传机制
异步上传初始化
ggml_backend_t upload_backend = [&]() -> ggml_backend_t {
if (use_mmap || check_tensors) {
return nullptr; // mmap或验证模式下不使用异步上传
}
// 检查backend是否支持异步上传
auto * buf = bufs.count(0) ? bufs.at(0) : nullptr;
if (!buf) return nullptr;
auto * buft = ggml_backend_buffer_get_type(buf);
auto * dev = ggml_backend_buft_get_device(buft);
if (!dev) return nullptr;
// 检查能力
ggml_backend_dev_props props;
ggml_backend_dev_get_props(dev, &props);
if (!props.caps.async || !props.caps.host_buffer || !props.caps.events) {
return nullptr;
}
// 创建pinned memory buffers
auto * host_buft = ggml_backend_dev_host_buffer_type(dev);
for (size_t idx = 0; idx < n_buffers; ++idx) {
auto * buf = ggml_backend_buft_alloc_buffer(host_buft, buffer_size);
host_buffers.emplace_back(buf);
host_ptrs.emplace_back(ggml_backend_buffer_get_base(buf));
auto * event = ggml_backend_event_new(dev);
events.emplace_back(event);
}
return ggml_backend_dev_init(dev, nullptr);
}();
异步上传流程
// 在load_all_data中
if (upload_backend) {
size_t offset = weight->offs;
size_t aligned_offset = offset & ~(alignment - 1);
size_t offset_from_alignment = offset - aligned_offset;
file->seek(aligned_offset, SEEK_SET);
size_t bytes_read = 0;
size_t data_read = 0;
while (bytes_read < read_end - read_start) {
size_t read_size = std::min<size_t>(buffer_size,
read_end - read_start - bytes_read);
// 等待上一个上传完成(避免覆盖)
ggml_backend_event_synchronize(events[buffer_idx]);
// 读取对齐块到pinned memory
file->read_raw_unsafe(host_ptrs[buffer_idx], read_size);
// 计算实际数据部分(排除对齐填充)
uintptr_t ptr_data = /* 对齐后的指针 */;
size_t data_to_copy = read_size;
if (bytes_read == 0) {
ptr_data += offset_from_alignment;
data_to_copy -= offset_from_alignment;
}
// 异步上传到GPU(不阻塞)
ggml_backend_tensor_set_async(upload_backend, cur,
reinterpret_cast<void *>(ptr_data), data_read, data_to_copy);
// 记录事件(用于后续同步)
ggml_backend_event_record(events[buffer_idx], upload_backend);
data_read += data_to_copy;
bytes_read += read_size;
buffer_idx = (buffer_idx + 1) % n_buffers; // 轮转缓冲区
}
}
优势:
- 重叠I/O和计算:读取下一块数据时,上一块正在上传
- 多缓冲区轮转:避免等待上传完成
- 对齐读取:提高I/O效率
内存管理优化
内存映射(mmap)优化
详见 内存映射(mmap)机制 章节。
补充优化点:
- 预填充页面:MAP_POPULATE标志立即读取所有页面(适用于小模型)
- 预读建议:POSIX_MADV_WILLNEED告诉OS预读页面
- NUMA感知:POSIX_MADV_RANDOM优化NUMA系统访问模式
内存锁定(mlock)机制
mlock将内存页面锁定在RAM中,防止被swap到磁盘。
struct llama_mlock {
void init(void * ptr) {
// 初始化mlock
}
void grow_to(size_t target_size) {
// 扩展锁定区域
#ifdef _POSIX_MEMLOCK_RANGE
if (mlock(ptr, target_size) != 0) {
// 处理错误(可能需要root权限)
}
#endif
}
};
使用场景:
- 实时推理系统(避免swap延迟)
- 需要root权限或CAP_IPC_LOCK能力
NUMA感知
NUMA(Non-Uniform Memory Access)系统需要优化内存访问模式。
// 在init_mappings中
bool is_numa = false;
auto * dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
if (dev) {
auto * reg = ggml_backend_dev_backend_reg(dev);
auto * is_numa_fn = (decltype(ggml_is_numa) *)
ggml_backend_reg_get_proc_address(reg, "ggml_backend_cpu_is_numa");
if (is_numa_fn) {
is_numa = is_numa_fn();
}
}
// 创建mmap时传递NUMA标志
std::unique_ptr<llama_mmap> mapping =
std::make_unique<llama_mmap>(file.get(), prefetch ? -1 : 0, is_numa);
NUMA优化:
- 使用POSIX_MADV_RANDOM提示OS随机访问模式
- 避免跨NUMA节点访问(如果可能)
内存碎片管理
llama.cpp通过以下策略减少内存碎片:
- 按Buffer类型分组:相同类型的tensor使用同一个context
- 对齐分配:tensor数据按16字节对齐
- mmap零拷贝:避免额外分配
架构特定加载逻辑
架构识别流程
// 1. 从GGUF读取架构名称
std::string arch_name;
ml.get_key(LLM_KV_GENERAL_ARCHITECTURE, arch_name, false);
// 2. 转换为枚举
llm_arch arch = llm_arch_from_string(arch_name);
// "llama" -> LLM_ARCH_LLAMA
// "qwen" -> LLM_ARCH_QWEN
// ...
// 3. 设置模型架构
model.arch = arch;
张量命名约定
llama.cpp使用统一的张量命名约定:
{prefix}.{layer_idx}.{tensor_name}.{suffix}
示例:
- tok_embeddings.weight (输入层)
- blk.0.attn_norm.weight (第0层,注意力归一化)
- blk.0.attn_q.weight (第0层,Query投影)
- blk.0.ffn_gate.weight (第0层,FFN门控)
- output_norm.weight (输出层)
- output.weight (输出权重)
架构特定前缀:
- Llama: blk.{i}
- Qwen: blk.{i}
- DeepSeek: blk.{i}
- Mamba: blk.{i} (但使用SSM层)
特殊架构处理
MoE架构(Mixtral/DeepSeek)
if (n_expert > 0) {
// 门控输入
layer.ffn_gate_inp = create_tensor(
tn(LLM_TENSOR_FFN_GATE_INP, "weight", i),
{n_embd, n_expert}, 0);
// 专家权重(3D张量:n_embd x n_ff x n_expert)
layer.ffn_gate_exps = create_tensor(
tn(LLM_TENSOR_FFN_GATE_EXPS, "weight", i),
{n_embd, n_ff, n_expert}, TENSOR_NOT_REQUIRED);
layer.ffn_up_exps = create_tensor(
tn(LLM_TENSOR_FFN_UP_EXPS, "weight", i),
{n_embd, n_ff, n_expert}, 0);
layer.ffn_down_exps = create_tensor(
tn(LLM_TENSOR_FFN_DOWN_EXPS, "weight", i),
{n_ff, n_embd, n_expert}, 0);
// 共享专家(DeepSeek)
if (hparams.n_ff_shexp > 0) {
layer.ffn_gate_shexp = create_tensor(...);
layer.ffn_up_shexp = create_tensor(...);
layer.ffn_down_shexp = create_tensor(...);
}
}
Mamba架构
case LLM_ARCH_MAMBA:
case LLM_ARCH_MAMBA2:
// SSM归一化
layer.ssm_norm = create_tensor(
tn(LLM_TENSOR_SSM_NORM, "weight", i), {n_embd}, 0);
// SSM参数
layer.ssm_dt_rank = create_tensor(...);
layer.ssm_in = create_tensor(...);
layer.ssm_out = create_tensor(...);
// ...
break;
RWKV架构
case LLM_ARCH_RWKV6:
case LLM_ARCH_RWKV7:
// RWKV使用循环层,不使用标准注意力
// 特殊的time_mix参数
layer.time_mix_k = create_tensor(...);
layer.time_mix_v = create_tensor(...);
// ...
break;
错误处理与进度跟踪
异常处理机制
try {
llama_model_loader ml(...);
model.load_arch(ml);
model.load_hparams(ml);
model.load_vocab(ml);
model.load_tensors(ml);
} catch (const std::exception & err) {
LLAMA_LOG_ERROR("error loading model: %s\n", err.what());
return -1;
}
常见错误:
- 文件不存在或格式错误
- 架构不支持
- 超参数缺失或无效
- 张量缺失或形状不匹配
- 内存不足
进度回调系统
typedef bool (*llama_progress_callback)(float progress, void * ctx);
// 在load_all_data中
if (progress_callback) {
float progress = (float) size_done / size_data;
if (!progress_callback(progress, progress_callback_user_data)) {
return false; // 用户取消
}
}
使用示例:
bool my_progress(float progress, void * ctx) {
printf("Loading: %.1f%%\r", progress * 100);
fflush(stdout);
return true; // 返回false可取消加载
}
llama_model_params params = llama_model_default_params();
params.progress_callback = my_progress;
params.progress_callback_user_data = nullptr;
取消加载机制
进度回调返回false时,加载会立即停止:
// 在load_all_data中
if (progress_callback) {
if (!progress_callback((float) size_done / size_data,
progress_callback_user_data)) {
return false; // 取消加载
}
}
// 在llama_model_load中
if (!model.load_tensors(ml)) {
return -2; // 返回-2表示取消
}
数据结构关联关系总结
加载链路关联
┌─────────────────────────────────────────────────────────────┐
│ 阶段 1: GGUF 读取 │
│ │
│ gguf_init_from_file() │
│ ↓ │
│ 解析所有 KV 对 → 存储在 gguf_context->kv 中 │
│ - "general.architecture" = "llama" │
│ - "llama.context_length" = 4096 │
│ - "llama.embedding_length" = 4096 │
│ - "llama.block_count" = 32 │
│ │
│ 结果:ml.meta (gguf_context_ptr) 持有所有 KV 对 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 2: 架构识别 │
│ │
│ model.load_arch(ml) │
│ ↓ │
│ ml.get_arch() → 从gguf_context读取架构名称 │
│ ↓ │
│ model.arch = LLM_ARCH_LLAMA │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 3: 超参数加载 │
│ │
│ model.load_hparams(ml) │
│ ↓ │
│ ml.get_key(LLM_KV_CONTEXT_LENGTH, hparams.n_ctx_train) │
│ ↓ │
│ 从gguf_context->kv查找并填充hparams │
│ │
│ 结果:model.hparams 包含所有数学参数 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 4: 张量创建 │
│ │
│ model.load_tensors(ml) │
│ ↓ │
│ 1. 根据hparams.n_layer创建layers数组 │
│ layers.resize(n_layer) │
│ │
│ 2. 从weights_map查找张量元数据 │
│ weight = ml.get_weight("blk.0.attn_q.weight") │
│ │
│ 3. 创建tensor对象(空壳) │
│ layer.wq = ml.create_tensor(ctx, tn, {n_embd, ...}, 0) │
│ │
│ 4. 分配设备 │
│ pimpl->dev_layer[0] = get_layer_buft_list(0) │
│ │
│ 结果:所有tensor对象已创建,但data仍为nullptr │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 阶段 5: 数据加载 │
│ │
│ ml.load_all_data() │
│ ↓ │
│ 遍历所有tensor: │
│ for (tensor in ctx) { │
│ weight = get_weight(tensor->name) │
│ if (use_mmap) { │
│ data = mapping->addr() + weight->offs │
│ ggml_backend_tensor_alloc(buf, tensor, data) │
│ // tensor->data = data (指向mmap地址) │
│ } else { │
│ file->read_raw(tensor->data, n_size) │
│ } │
│ } │
│ │
│ 结果:tensor->data指向实际权重数据 │
└─────────────────────────────────────────────────────────────┘
数据结构职责划分
| 数据结构 | 职责 | 生命周期 |
|---|---|---|
| gguf_context | GGUF文件元数据存储 | 加载期间 |
| llama_model_loader | 权重索引、文件管理、数据加载 | 加载期间 |
| llama_hparams | 数学参数存储 | 模型生命周期 |
| llama_model | 权重指针、设备管理、元数据 | 模型生命周期 |
| llama_layer | 单层权重指针 | 模型生命周期 |
| ggml_tensor | 张量元数据+数据指针 | 模型生命周期 |
| ggml_backend_buffer | 物理内存管理 | 模型生命周期 |
关键设计模式
- Pimpl模式:隐藏实现细节,保持API稳定
- 索引表模式:weights_map作为”目录”,快速定位权重
- 策略模式:设备分配、Buffer选择通过策略函数实现
- 工厂模式:create_tensor根据架构创建不同张量
总结
GGUF读取的调用链
GGUF文件的读取在llama.cpp中遵循以下调用链:
llama_model_load_from_file_impl()
↓
llama_model_load()
↓
llama_model_loader::llama_model_loader() // 构造函数
↓
gguf_init_from_file() // 打开文件并调用解析函数
↓
gguf_init_from_file_impl() // 实际解析函数
↓
gguf_reader // 文件读取器
关键调用点:
// 在 llama_model_loader 构造函数中(llama-model-loader.cpp:531)
struct gguf_init_params params = {
/*.no_alloc = */ true, // 只读取元数据,不分配tensor数据
/*.ctx = */ &ctx, // 返回ggml_context用于存储tensor元数据
};
meta.reset(gguf_init_from_file(fname.c_str(), params));
// meta 是 gguf_context_ptr,持有解析后的GGUF元数据
GGUF解析的详细流程
gguf_init_from_file_impl 按以下顺序解析文件:
步骤1:校验Magic Number
std::vector<char> magic;
gr.read(magic, 4); // 读取4字节
// 验证是否为 "GGUF"
步骤2:读取文件头
gr.read(ctx->version); // 读取版本号(4字节)
gr.read(n_kv); // 读取KV对数量(8字节)
gr.read(n_tensors); // 读取张量数量(8字节)
步骤3:解析所有KV对
for (int64_t i = 0; i < n_kv; ++i) {
std::string key = gr.read_string(); // 读取键名
gguf_type type = (gguf_type)gr.read_u32(); // 读取类型
// 根据类型读取值
switch (type) {
case GGUF_TYPE_UINT8: ctx->kv.emplace_back(key, gr.read_u8()); break;
case GGUF_TYPE_INT32: ctx->kv.emplace_back(key, gr.read_i32()); break;
case GGUF_TYPE_FLOAT32: ctx->kv.emplace_back(key, gr.read_f32()); break;
case GGUF_TYPE_STRING: ctx->kv.emplace_back(key, gr.read_string()); break;
// ... 更多类型
}
}
// 结果:所有KV对存储在 ctx->kv 向量中
步骤4:解析所有张量信息
for (int64_t i = 0; i < n_tensors; ++i) {
struct gguf_tensor_info info;
info.name = gr.read_string(); // 张量名称
info.n_dims = gr.read_u32(); // 维度数
for (uint32_t j = 0; j < info.n_dims; ++j) {
info.dims[j] = gr.read_u64(); // 各维度大小
}
info.type = (ggml_type)gr.read_u32(); // 数据类型
info.offset = gr.read_u64(); // 数据偏移(相对数据区)
ctx->info.push_back(info);
}
// 结果:所有张量信息存储在 ctx->info 向量中
步骤5:计算数据区偏移
ctx->offset = gr.tell(); // 当前位置就是数据区开始位置
// 此时文件指针已跳过所有元数据,指向实际权重数据
步骤6:创建GGML Context和Tensor元数据(如果no_alloc=false)
if (params.ctx && !params.no_alloc) {
// 创建ggml_context
ggml_context * ctx_data = ggml_init({...});
// 为每个tensor创建ggml_tensor对象
for (size_t i = 0; i < ctx->info.size(); ++i) {
struct ggml_tensor * cur = ggml_new_tensor(ctx_data, ...);
ggml_set_name(cur, info.t.name);
// 如果no_alloc=false,将data指针指向文件中的数据位置
cur->data = (char *) data->data + info.offset;
}
}
读取后存储的数据结构
GGUF解析后的数据存储在以下结构中:
gguf_context(核心存储结构)
struct gguf_context {
uint32_t version; // GGUF版本
std::vector<gguf_kv> kv; // 所有KV对
// 例如:
// kv[0] = {"general.architecture", "llama"}
// kv[1] = {"llama.context_length", 4096}
// kv[2] = {"llama.embedding_length", 4096}
// ...
std::vector<gguf_tensor_info> info; // 所有张量信息
// 例如:
// info[0] = {name: "tok_embeddings.weight", dims: [4096, 32000], type: F16, offset: 0}
// info[1] = {name: "blk.0.attn_q.weight", dims: [4096, 4096], type: F16, offset: 131072000}
// ...
size_t alignment; // 对齐字节数(通常16或32)
size_t offset; // 数据区在文件中的偏移
void * data; // 数据区指针(如果已映射)
};
llama_model_loader中的存储
struct llama_model_loader {
gguf_context_ptr meta; // 持有gguf_context
// meta.get() 返回 gguf_context*,包含所有KV对和张量信息
llama_files files; // 文件句柄列表
// files[0] = 主文件
// files[1..n] = 分片文件(如果有)
std::map<std::string, llama_tensor_weight> weights_map;
// 权重索引表,Key是tensor名称,Value包含:
// - idx: 文件索引(0=主文件,1..n=分片)
// - offs: 在文件中的字节偏移
// - tensor: 指向ggml_tensor元数据
};
在llama.cpp中的调用流程
完整调用链:
// 1. 用户调用入口
llama_model * model = llama_model_load_from_file("model.gguf", params);
// 2. 内部调用链
llama_model_load_from_file_impl()
→ llama_model_load()
→ llama_model_loader ml(fname, ...) // 构造函数开始解析GGUF
→ gguf_init_from_file(fname, params)
→ gguf_init_from_file_impl(file, params)
// 解析文件头、KV对、张量信息
// 返回 gguf_context*
→ meta.reset(gguf_context*) // 保存到loader中
→ 遍历ctx中的tensor,构建weights_map
→ 处理分片文件(如果有)
// 3. 后续使用
model.load_arch(ml) // 从meta读取架构名称
model.load_hparams(ml) // 从meta读取超参数
model.load_vocab(ml) // 从meta读取词表
model.load_tensors(ml) // 从weights_map定位权重,从文件读取数据
关键访问接口:
// 在 llama_model_loader 中访问KV对
ml.get_key(LLM_KV_CONTEXT_LENGTH, hparams.n_ctx_train);
// 内部调用:
// gguf_find_key(meta.get(), "llama.context_length")
// gguf_get_val_u32(meta.get(), key_id)
// 访问张量元数据
ggml_tensor * t_meta = ml.get_tensor_meta("blk.0.attn_q.weight");
// 内部从weights_map查找,返回ggml_tensor*(只有元数据,data为nullptr)
// 访问权重位置
const auto * weight = ml.get_weight("blk.0.attn_q.weight");
// 返回 llama_tensor_weight,包含:
// weight->idx = 0(主文件)
// weight->offs = 131072000(文件偏移)
// weight->tensor = ggml_tensor*(元数据)
数据流转图
┌───────────────────────────────────────────────┐
│ GGUF文件(磁盘) │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Magic │ Version │ n_kv │ KV Pairs │ │
│ └─────────┴─────────┴─────────┴─────────┘ │
│ ┌─────────┬─────────────────────────────┐ │
│ │n_tensors │ Tensor Info Array │ │
│ └─────────┴─────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ Tensor Data (实际权重) │ │
│ └───────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
↓ gguf_init_from_file()
┌────────────────────────────────────────────┐
│ gguf_context(内存) │
│ │
│ ctx->kv[]: │
│ [0] = {"general.architecture", "llama"} │
│ [1] = {"llama.context_length", 4096} │
│ [2] = {"llama.embedding_length", 4096} │
│ ... │
│ │
│ ctx->info[]: │
│ [0] = {name: "tok_embeddings.weight", offset: 0, ...} │
│ [1] = {name: "blk.0.attn_q.weight", offset: 131072000,...} │
│ ... │
│ │
│ ctx->offset = 数据区偏移 │
└────────────────────────────────────────────┘
↓ 存储到 llama_model_loader
┌────────────────────────────────────────────┐
│ llama_model_loader │
│ │
│ meta: gguf_context_ptr → 持有所有KV和tensor信息 │
│ │
│ weights_map: │
│ "tok_embeddings.weight" → {idx:0, offs:0, tensor:...} │
│ "blk.0.attn_q.weight" → {idx:0, offs:131072000, tensor:...} │
│ ... │
│ │
│ files: [主文件句柄, 分片1句柄, ...] │
└────────────────────────── ─────────────────┘
↓ 使用
┌───────────────────────────────────────────┐
│ 模型加载各阶段 │
│ │
│ load_arch(): 从 meta->kv 读取架构名称 │
│ load_hparams(): 从 meta->kv 读取超参数 │
│ load_vocab(): 从 meta->kv 读取词表 │
│ load_tensors(): 从 weights_map 定位权重,从文件读取数据 │
└───────────────────────────────────────────┘