llama.cpp 模型加载机制深度解析

🕒 2026-02-12 📁 推理框架 👤 laumy 🔥 153 热度

概述

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;
};

核心设计点

  1. 结构化抽象 – layers数组
  • std::vector layers 是核心设计
  • 将重复的层权重封装到 llama_layer 中
  • 支持动态层数,无需修改结构体定义
  1. 设备管理
  • devices 向量记录所有使用的后端设备
  • dev_layer(int il) 返回第 il 层所在的设备
  • dev_output() 返回输出层所在的设备
  • 支持多GPU分层加载
  1. Pimpl模式
  • 隐藏底层实现细节(mmap管理、内存映射句柄等)
  • 保持API稳定性,修改实现不影响头文件
  1. 权重指针而非数据
  • 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;

    // ... 更多架构特定张量
};

设计特点

  1. 通用层抽象
  • 合并了对Gemma、Mixtral、Mamba、RWKV、DeepSeek等模型的支持
  • 通过指针是否为nullptr判断架构是否使用该张量
  1. 架构变体支持
  • 归一化变体:支持RMSNorm、LayerNorm、Gemma 2的特殊归一化
  • 注意力变体:标准QKV、合并QKV、分组查询注意力(GQA)
  • MoE支持:专家门控、共享专家(Shared Experts)
  • 非Transformer:Mamba的SSM层、RWKV的循环层
  1. 加载流程
  • 预分配: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)
  • 按层号排序,而非纯字符串排序
  • 优化磁盘读取顺序,利用预读缓存

关键流程

  1. 构造函数:初始化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. 处理分片文件(如果有)
       // ...
    }
    
  2. 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));
               }
           }
       }
    }
    
  3. create_tensor:创建张量对象(空壳)

  • 从weights_map查找元数据
  • 在指定的ggml_context中创建tensor对象
  • 此时tensor->data仍为nullptr
  1. 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 存储模型的数学参数,决定计算图的形状和逻辑。

核心参数分类

  1. 基础架构参数
  • n_embd: 隐藏层维度(如4096)
  • n_layer: 总层数
  • n_ctx_train: 训练时上下文长度
  • n_head_arr[]: 每层注意力头数(数组支持每层不同)
  • n_head_kv_arr[]: 每层KV头数(GQA支持)
  1. 注意力机制变体
  • n_embd_head_k_mla: MLA压缩后的KV维度(DeepSeek-V2/V3)
  • n_swa: 滑动窗口大小(Mistral)
  • swa_layers[]: 哪些层启用滑动窗口
  1. 位置编码(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)
  1. 混合专家模型(MoE)
  • n_expert: 总专家数
  • n_expert_used: 每个token激活的专家数(如8选2)
  • n_ff_exp: 专家FFN维度
  • n_ff_shexp: 共享专家维度(DeepSeek)
  • expert_gating_func: 门控函数类型
  1. 状态空间模型(SSM/Mamba)
  • ssm_d_conv: 卷积维度
  • ssm_d_inner: 内部维度
  • ssm_d_state: 状态维度
  • recurrent_layer_arr[]: 哪些层是循环层(混合架构)
  1. 归一化与数值稳定性
  • 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要独立出来?

  1. 多版本适配:不同架构可能完全没有某些参数(如Mamba没有n_head),但都有n_embd
  2. KV Cache管理:hparams决定KV Cache的内存布局和大小
  3. 计算图构建:推理时算子需要从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
}

架构识别流程

  1. GGUF文件头包含 general.architecture KV对
  2. 读取字符串值(如”llama”、”qwen”、”deepseek”)
  3. 通过 llm_arch_from_string() 转换为枚举
  4. 如果无法识别,抛出异常

阶段二:超参数加载

详见 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;
}

设备分配策略

  1. 输入层:始终在CPU(dev_input = cpu_dev)
  2. 中间层:根据n_gpu_layers和tensor_split分配
  3. 输出层:与最后一层相同设备

分割计算

// 默认分割:按显存比例
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优势

  1. 零拷贝:文件数据直接映射到虚拟地址空间,无需拷贝
  2. 延迟加载:页面在首次访问时才从磁盘读取(按需分页)
  3. 内存共享:多个进程可以共享同一份映射,节省内存
  4. 操作系统优化: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();
}

选择策略

  1. 优先选择GPU buffer(如果支持该操作)
  2. 回退到Host buffer(用于传输)
  3. 最后回退到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)机制 章节。

补充优化点:

  1. 预填充页面:MAP_POPULATE标志立即读取所有页面(适用于小模型)
  2. 预读建议:POSIX_MADV_WILLNEED告诉OS预读页面
  3. 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通过以下策略减少内存碎片:

  1. 按Buffer类型分组:相同类型的tensor使用同一个context
  2. 对齐分配:tensor数据按16字节对齐
  3. 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 物理内存管理 模型生命周期

关键设计模式

  1. Pimpl模式:隐藏实现细节,保持API稳定
  2. 索引表模式:weights_map作为”目录”,快速定位权重
  3. 策略模式:设备分配、Buffer选择通过策略函数实现
  4. 工厂模式: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 定位权重,从文件读取数据              │
└───────────────────────────────────────────┘

发表你的看法

\t