GGML源码逐行调试(上)

news2025/4/14 19:38:07

目录

    • 前言
    • 1. 简述
    • 2. 环境配置
    • 3. ggml核心概念
      • 3.1 gguf
      • 3.2 ggml_tensor
      • 3.3 ggml_backend_buffer
      • 3.4 ggml_context
      • 3.5 backend
      • 3.6 ggml_cgraph
      • 3.7 ggml_gallocr
    • 4. 推理流程整体梳理
      • 4.1 时间初始化与参数设置
      • 4.2 模型加载与词汇表构建
      • 4.3 计算图与内存分配
      • 4.4 文本预处理与推理过程
      • 4.5 性能统计与资源释放
      • 4.6 完整推理流程
    • 结语
    • 下载链接
    • 参考

前言

学习 UP 主 比飞鸟贵重的多_HKL 的 GGML源码逐行调试 视频,记录下个人学习笔记,仅供自己参考😄

refer1:【大模型部署】GGML源码逐行调试

refer2:llama.cpp源码解读–ggml框架学习

refer3:https://github.com/ggml-org/ggml

refer4:https://chatgpt.com/

1. 简述

本来想直接上手学习 llama.cpp 的,但是在读源码时发现很多概念都没接触过,所以这里还是花一些时间从简单的 ggml 框架开始学习

ggml 是一个用 c/c++ 编写、专注于 transformer 架构模型推理的机器学习库,它和 pytorch、tensorflow 等机器学习库比较类似。github 地址是:https://github.com/ggml-org/ggml

关于 ggml 更多的介绍大家可以参考 Georgi Gerganov 在 hugging face 上发布的一篇文章:Introduction to ggml

2. 环境配置

ggml 本身没有什么依赖,并且它的 README 文档对其环境配置已经描述得很清楚了,需要注意的是 ggml 提供了很多案例,例如 sgemm、mnist、yolo、sam、gpt-2 等等,这里我们主要分析的案例是 gpt-2 的推理

下面我们来简单过下如何利用 ggml 推理 gpt-2

1. 代码克隆,指令如下:

git clone https://github.com/ggml-org/ggml

Note:大家也可以点击 here 下载博主准备好的源码(注意该代码下载于 2025/4/12 日,若后续有改动请参考最新

2. 模型下载,指令如下:

cd ggml
bash examples/gpt-2/download-ggml-model.sh 117M

等待一段时间后,在 models/gpt-2-117M 文件夹下会保存 ggml-model.bin 的模型文件

Note:模型下载需要访问外网,大家也可以点击 here 下载博主准备好的模型

3. 代码编译,指令如下:

cd ggml
mkdir build && cd build
cmake ..
cmake --build . --config Release -j 8

4. 执行推理,指令如下:

cd ggml
./build/bin/gpt-2-backend -m models/gpt-2-117M/ggml-model.bin -p "This is an example"

这里我们指定了模型的路径,并且给出了我们的 prompt 提示词 "This is an example"

输出如下图所示:

在这里插入图片描述

从上图中可以看到,我们成功执行了推理,此外终端输出的并不只有模型推理的结果,还包括很多其他的东西,例如模型加载时的一些信息打印、推理的耗时打印等等

值得注意的是推理时默认使用的后端是 CPU 后端,如果大家有 GPU 可以选择 CUDA 后端,我们调试时也是以 CUDA 后端来分析的

下面我们就来看看如何使用 CUDA 后端推理,指令如下:

cd ggml
mkdir build && cd build
cmake -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=/usr/local/cuda-11.8/bin/nvcc ..
cmake --build . --config Release -j 8
cd ..
./build/bin/gpt-2-backend -m models/gpt-2-117M/ggml-model.bin -p "This is an example" -ngl 12

Note:其中 -DCMAKE_CUDA_COMPILER 参数指定的 nvcc 路径需要替换为你自己电脑上的路径

如果需要支持 CUDA 后端也很容易,只需要在编译时将 -DGGML_CUDA 开启,同时指定下 nvcc 的路径即可。另外在推理时需要新增一个 -ngl 的参数,它代表的含义是 number of layers to offload to GPU on supported models,也就是将模型的多少个 layer 放到 GPU 上去推理,默认 0

值得注意的是如果不指定 -ngl 参数,即便你在编译时开启了 CUDA 后端支持,在执行时也是不会在 GPU 上推理,因为模型加载函数中有如下的代码:

#ifdef GGML_USE_CUDA
    if (n_gpu_layers > 0) {
        fprintf(stderr, "%s: using CUDA backend\n", __func__);
        model.backend = ggml_backend_cuda_init(0);
        if (!model.backend) {
            fprintf(stderr, "%s: ggml_backend_cuda_init() failed\n", __func__);
        }
    }
#endif

我们可以从上述代码中发现,它通过宏定义 GGML_USE_CUDA 来判断是否使用 CUDA 后端来推理模型,此外只有当 n_gpu_layes 参数大于 0 时才会做 CUDA 后端的初始化,这里的 n_gpu_layes 变量就是我们上面提到的 -ngl 参数

CUDA 后端输出如下图所示:

在这里插入图片描述

CUDA 后端推理时在终端可以看到 GPU 设备的一些信息,另外可以看到每个 token 预测所使用的时间明显减少了

OK,以上就是利用 ggml 推理 gpt-2 时环境配置的整体流程了

既然要学习 ggml 的源码,肯定是需要 debug 逐行调试的,因此下面我们来配置下 debug 时的环境

Note:博主使用的是 vscode 编辑器,如果大家使用的是其它工具,可能需要自行配置

1. Debug 模式编译,指令如下:

cd ggml
mkdir build && cd build
cmake -DGGML_CUDA=ON -DCMAKE_CUDA_COMPILER=/usr/local/cuda-11.8/bin/nvcc -DCMAKE_BUILD_TYPE=Debug ..
cmake --build . -j 8

2. vscode 配置 c++ 的调试功能

主要是在 .vscode 文件夹下新建 tasks.json、launch.json 以及 c_cpp_properties.json 来进行调试,其中 c_cpp_properties.json 非必需,下面是它们的一些说明:

  • .vscode:它是一个隐藏文件夹,其主要作用是以 .json 的方式保存相关配置信息,如编译任务的配置、调试任务的配置、工作环境的配置等等
  • tasks.json:用来配置编译文件的信息,用于生成可执行文件
  • launch.json:用来配置调试文件的信息,比如指定调试语言环境、指定调试类型等等
  • c_cpp_properties.json:配置 c/c++ 代码的智能提示,方便代码的书写

tasks.json 内容如下:

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",

            // for cmake
            "command": "cd build && cmake --build . -j 8"
        }
    ]
}

launch.json 内容如下:

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "(gdb) 启动",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/bin/gpt-2-backend",
            "args": [
                "-m", "models/gpt-2-117M/ggml-model.bin",
                "-p", "This is an example",
                "-ngl", "12"
            ],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "为 gdb 启用整齐打印",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            "preLaunchTask": "build"
        }
    ]
}

c_cpp_properties.json 内容如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/include/**",
                "${workspaceFolder}/examples/**",
                "${workspaceFolder}/src/**",
                "/usr/local/cuda-11.8/include/**"
            ],
            "defines": ["__CUDACC__", "HAS_PYTHON"],
            "compilerPath": "/usr/bin/gcc", 
            "cStandard": "gnu11",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64",
            "configurationProvider": "ms-vscode.makefile-tools",
            "browse": {
                "path": [
                    "${workspaceFolder}/include/**",
                    "${workspaceFolder}/examples/**",
                    "${workspaceFolder}/src/**",
                    "/usr/local/cuda-11.8/include/**"
                ],
                "limitSymbolsToIncludedHeaders": false,
                "databaseFilename": ""
            }
        }
    ],
    "version": 4
}

Note:注意将 CUDA 头文件目录修改为你自己的路径

3. 打断点,进入调试

我们需要调试的文件是 main-backend.cpp,位于 examples/gpt-2 文件夹下,找到这个文件后在 main 函数中打一个断点,将上面的环境配置好后,按住 F5 即可进入调试,如下图所示:

在这里插入图片描述

接下来我们就可以愉快的调试代码了🤗

3. ggml核心概念

在分析调试代码之前,我们先来简单的梳理下 ggml 中的一些核心概念,博主本来是想放在代码中一边讲解一边梳理的,但是发现如果不事先了解下这些概念的话,在调试阅读源码时会比较困难

3.1 gguf

gguf 是一种文件格式,由 Georgi Gerganov 定义发布,用于存储使用 ggml 框架进行推理的模型文件。gguf 是一种二进制格式,旨在快速加载和保存模型。大部分模型是通过 pytorch 或者其他框架开发的,我们可以将它们转换成 gguf 格式并通过 ggml 框架推理

博主对于 ggml 框架中 LLM 的存储方式有些困惑,为什么不使用像 ONNX 这种能够直接查看计算图的存储格式,而是使用单独定义的 GGUF 格式呢?🤔

可能的原因有以下几点:(from ChatGPT)

1. 精细化的量化与推理优化

ggml 的设计初衷就是通过高效的量化和特殊的内存布局来实现本地 CPU/GPU 上的快速推理

  • 常见的 ONNX 推理引擎也支持 FP16/INT8 等常用量化格式,但 ggml/llama.cpp 社区针对大模型场景做了许多的优化,例如 Q4_0、Q4_K、Q5_0、Q8_0 等更细粒度的专用量化方案
  • ggml 在推理或加载时的某些操作不一定能直接在 ONNX 解析器中被高效地识别和执行。也就是说,如果使用 ONNX,仍然需要再去做针对性 “二次开发” 或适配,才能在内存带宽受限的环境中跑得足够快

2. 最小化对外部依赖和格式解析的需求

ONNX 文件的结构本身是基于 Protocol Buffers(protobuf),里面包含了大量描述算子的元数据、模型结构等,一个完整的 ONNX 模型包含算子定义(opset)、计算图、以及用于图推理的各种配置信息

  • ggml/llama.cpp 这种使用场景往往是想把一切都打包到非常简洁的推理实现里,不依赖过多外部库,也不需要通用推理引擎去解析或执行复杂算子
  • 只需要保留 “大语言模型的参数和最小必要的结构信息”,就可以完成推理,避免处理通用 ONNX 解析框架的开销和复杂度

3. 灵活的模型结构与定制的推理过程

大语言模型的推理过程是一个相对固定的 Transformer 堆叠(如解码器结构),而 ONNX 这种通用格式会把所有网络层、激活函数等一股脑儿封装在计算图中

  • ggml/llama.cpp 以及衍生项目中,推理的具体步骤、batch 处理、KV Cache 等,往往都放在手写或者定制的 C/C++/Go/Python 逻辑里实现
  • 由于模型结构和推理过程已经非常固定且可预测,所以常常不需要一个通用的 “模型结构描述 + 解析器” 来动态构建推理图。这时,存储权重时只要能快速映射到内存,按照适合的张量布局进行运算就足够了

4. 文件体积与加载速度

对大语言模型而言,模型文件本身动辄几十 GB。如果我们使用的是经过高度量化(如 Q4、Q5 等)的权重,加载时只需要非常小且固定的 “额外数据” 即可开始推理,减少了对复杂解析的依赖

  • 在 ONNX 中,权重依旧要打包到同一个 protobuf 中,但如何高效地组织这些量化后的张量(并尽量减少不必要的结构信息)就相对繁琐
  • GGUF 则是专为量化权重在本地高效加载而生,为此精心做了格式上的取舍、适配与优化

5. 更适合「社区量化版本」与开源生态

ggml/llama.cpp 以及对应的 GGUF 文件,能非常快速地衍生出社区版本:

  • 比如将原始模型转换成多种量化精度版本,只需要在转换脚本中调用相关函数,就能快速把原始 FP32/FP16 权重转成 Q4、Q5、Q8 等多种量化精度
  • 再加上 GGUF 文件通常很轻量,格式相对简单,社区可以无需 ONNXRuntime、TensorRT 等推理引擎,也能低门槛地进行本地推理

总体来说,ONNX 适合更通用、更跨平台的模型交换,尤其在多框架支持和通用推理场景下非常有用。但是像 ggml/llama.cpp 这类专注于 “在 CPU 或 GPU 上高效推理大语言模型” 并且大量使用细粒度量化策略的场景,就会选择与自身开发思路契合的专用格式(如 GGUF),省去对通用计算图进行解析和适配的麻烦,也能高度控制在加载、推理时的内存布局和性能表现

gguf 文件结构如下图所示:

在这里插入图片描述

gguf 文件中各字节代表的含义如上图所示,它们使用在 general.alignment 元数据字段中指定的全局对齐方式,在需要时,gguf 文件会以 0x00 字节填充到 general.alignment 的下一个倍数。除非另有说明,否则字段(包括数组)都是按顺序写入的,不会对齐

关于 gguf 更详细的说明,大家感兴趣的可以看看官方说明文档:https://github.com/ggml-org/ggml/blob/master/docs/gguf.md

3.2 ggml_tensor

ggml_tensor 是 ggml 框架中用于表示 n 维张量(tensor)的结构体,可以类比于 pytorch 中 tensor 的概念

其定义如下:

/* include/ggml.h */

// n-dimensional tensor
struct ggml_tensor {
enum ggml_type type;

struct ggml_backend_buffer * buffer;

int64_t ne[GGML_MAX_DIMS]; // number of elements
size_t  nb[GGML_MAX_DIMS]; // stride in bytes:
                                // nb[0] = ggml_type_size(type)
                                // nb[1] = nb[0]   * (ne[0] / ggml_blck_size(type)) + padding
                                // nb[i] = nb[i-1] * ne[i-1]

// compute data
enum ggml_op op;

// op params - allocated as int32_t for alignment
int32_t op_params[GGML_MAX_OP_PARAMS / sizeof(int32_t)];

int32_t flags;

struct ggml_tensor * src[GGML_MAX_SRC];

// source tensor and offset for views
struct ggml_tensor * view_src;
size_t               view_offs;

void * data;

char name[GGML_MAX_NAME];

void * extra; // extra things e.g. for ggml-cuda.cu

char padding[8];
};

其中:(from ChatGPT)

  • type 字段表示张量的数据类型,它是一个枚举类(ggml_type)。ggml 支持多种数据类型,例如 GGML_TYPE_F32GGML_TYPE_F16GGML_TYPE_Q4_0 等等
  • buffer 字段是一个指向 ggml_backend_buffer 结构的指针,指向的是能够操作该张量对应的数据块的接口,而实际 tensor 对应的数据存储地址为 data 指针指向的位置。该结构体我们后续会做详细分析
  • ne 是一个整数数组,表示张量每一维的大小。例如,对于 3 维张量,ne 数组将包含三个数值分别表示每一维的大小
  • nb 数组表示每一维度的字节步长(stride)。步长用于描述内存中不同维度元素的存储距离,例如,nb[0] 是类型元素的大小,nb[1] 是跨越第一维度所需的字节数,依此类推。它用于高效地访问多维数组中的元素,对于一个 4x3x2 的 tensor 来说,其步长 nb 计算如下:

在这里插入图片描述

  • op 字段表示张量的操作类型,它也是一个枚举类(ggml_op),将其设置为 GGML_OP_NONE 时表示该 tensor 持有数据。而其它值表示不同的操作,例如 GGML_OP_MUL_MAT 表示该 tensor 不保存数据,只表示其它两个 tensor 之间的矩阵乘法结果
  • op_params 是一个整数数组,存储与张量操作相关的一些参数
  • flags 字段存储张量的一些状态标志,可能用于标识张量的特性,如是否需要梯度计算等
  • src 是一个指针数组,指向要在其间进行运算的 tensor。例如,如果 op == GGML_OP_MUL_MAT,那么 src 将包含指向两个要相乘的 tensor 的指针,如果 op == GGML_OP_NONE,那么 src 将是空的
  • view_src 字段表示另一个张量的指针,若该 tensor 是其它 tensor 的引用时,引用的目标 tensor 指针为 view_src
  • view_offs 字段表示偏移量,即从 view_src 中的哪个位置开始获取数据
  • data 指向实际的 tensor 数据,如果该 tensor 是一个操作(例如 op == GGML_OP_MUL_MAT),则指向 NULL。它也可以指向另一个 tensor 的数据,即视图(view)
  • name 是一个字符数组,用于存储张量的名词
  • extra 是一个通用的指针,允许为张量存储额外的信息。在 CUDA 或其它后端中,它可能用于存储特定于设备的额外数据
  • padding 是用来填充的空间,可能是为了确保结构体对齐,以便更高效地访问内存
    • 关于结构体对齐是怎么计算的,大家感兴趣的可以看看:结构体对齐怎么计算的,一个视频带你理解清楚

ggml_tensor 结构体定义了一个 n 维张量的各种属性,包括张量的维度信息、数据类型、内存存储、计算操作、源张量等。它主要涉及数据存储、计算图操作和后端支持(如 CUDA)的交互

在 理解llama.cpp如何进行LLM推理 文章中我们也有详细解释过 ggml 中的张量,大家感兴趣的可以看看

3.3 ggml_backend_buffer

在 ggml 框架中,一切数据(context、dataset、weight、output…)都应该被存放在 buffer 中,而之所以要用 buffer 进行集成承载不同的数据,主要是为了便于实现多种后端(CPU、GPU)设备内存的统一管理。也就是说 buffer 是实现不同类型数据在多种后端上进行统一的接口对象,其结构如下图所示:

在这里插入图片描述

在 ggml 框架中,buffer 结构体 ggml_backend_buffer 的设计涉及了较多的层次,使用了面向对象的编程模式来封装不同的功能。这种设计模式的核心目的是为每种设备(例如 CPU、GPU)提供一个通用的接口,同时又能根据设备的特性执行优化

其定义如下:

/* src/ggml-backend-impl.h */

struct ggml_backend_buffer {
    struct ggml_backend_buffer_i  iface;
    ggml_backend_buffer_type_t    buft;
    void * context;
    size_t size;
    enum ggml_backend_buffer_usage usage;
};

我们可以按照以下几个关键组件来分析整个 buffer 的设计:

1. ggml_backend_buffer_i

ggml_backend_buffer_i 是一个结构体,其定义如下:

struct ggml_backend_buffer_i {
    // (optional) free the buffer
    void         (*free_buffer)  (ggml_backend_buffer_t buffer);
    // base address of the buffer
    void *       (*get_base)     (ggml_backend_buffer_t buffer);
    // (optional) initialize a tensor in the buffer (eg. add tensor extras)
    void         (*init_tensor)  (ggml_backend_buffer_t buffer, struct ggml_tensor * tensor);
    // tensor data access
    void         (*memset_tensor)(ggml_backend_buffer_t buffer,       struct ggml_tensor * tensor,     uint8_t value, size_t offset, size_t size);
    void         (*set_tensor)   (ggml_backend_buffer_t buffer,       struct ggml_tensor * tensor, const void * data, size_t offset, size_t size);
    void         (*get_tensor)   (ggml_backend_buffer_t buffer, const struct ggml_tensor * tensor,       void * data, size_t offset, size_t size);
    // (optional) tensor copy: dst is in the buffer, src may be in any buffer, including buffers from a different backend (return false if not supported)
    bool         (*cpy_tensor)   (ggml_backend_buffer_t buffer, const struct ggml_tensor * src, struct ggml_tensor * dst);
    // clear the entire buffer
    void         (*clear)        (ggml_backend_buffer_t buffer, uint8_t value);
    // (optional) reset any internal state due to tensor initialization, such as tensor extras
    void         (*reset)        (ggml_backend_buffer_t buffer);
};

它定义了一组指针函数(即接口),这些函数是针对缓冲区(buffer)操作的一些基本功能。_i 后缀通常表示 “interface”(接口),它是一种编程约定,用于区分结构体与它所提供的接口函数

这个结构体中的函数指针如下:

  • free_buffer:释放缓冲区
  • get_base:获取缓冲区的首地址
  • init_tensor:在缓冲区中初始化张量
  • memset_tensorset_tensorget_tensor:用于张量数据的读写
  • cpy_tensor:在不同的缓冲区间进行张量数据的复制
  • clear:清空整个缓冲区
  • reset:重置缓冲区的内部状态

这些操作都是通过不同后端设备实现的具体细节来执行的

Note:结构体中的 ggml_backend_buffer_tggml_backend_buffer 结构体的指针类型,_t 后缀通常表示它是一个类型别名

2. ggml_backend_buffer_type_t

ggml_backend_buffer_type_t 是指向 ggml_backend_buffer_type 的指针类型,而 ggml_backend_buffer_type 结构体定义如下:

struct ggml_backend_buffer_type {
    struct ggml_backend_buffer_type_i  iface;
    ggml_backend_dev_t device;
    void * context;
};

该结构体用于表示缓冲区的类型,每种缓冲区类型都会实现一组接口(通过结构体 ggml_backend_buffer_type_i 来定义),允许在不同的后端(如 CPU 和 GPU)上创建和管理缓冲区

Noteggml_backend_dev_t 是指向 ggml_backend_device 的指针类型,它封装了后端设备的各种具体信息

ggml_backend_buffer_type_i 定义如下:

struct ggml_backend_buffer_type_i {
    const char *          (*get_name)      (ggml_backend_buffer_type_t buft);
    // allocate a buffer of this type
    ggml_backend_buffer_t (*alloc_buffer)  (ggml_backend_buffer_type_t buft, size_t size);
    // tensor alignment
    size_t                (*get_alignment) (ggml_backend_buffer_type_t buft);
    // (optional) max buffer size that can be allocated (defaults to SIZE_MAX)
    size_t                (*get_max_size)  (ggml_backend_buffer_type_t buft);
    // (optional) data size needed to allocate the tensor, including padding (defaults to ggml_nbytes)
    size_t                (*get_alloc_size)(ggml_backend_buffer_type_t buft, const struct ggml_tensor * tensor);
    // (optional) check if tensor data is in host memory and uses standard ggml tensor layout (defaults to false)
    bool                  (*is_host)       (ggml_backend_buffer_type_t buft);
};

其接口函数包括:

  • get_name:获取缓冲区类型名
  • alloc_buffer:为指定类型分配缓冲区
  • get_alignment:获取缓冲区的对齐要求
  • get_max_sizeget_alloc_size:查询缓冲区的最大大小和分配大小
  • is_host:检查缓冲区是否位于主机内存 host 中

3. contextsize

context 字段是一个指向 void 的指针,通常用于存储与特定后端或设备相关的额外信息,可能是设备上下文、内存管理上下文或者其他一些元数据

size 字段则表示缓冲区的大小

4. ggml_backend_buffer_usage

ggml_backend_buffer_usage 是一个枚举类型,用于描述缓冲区的用途,其定义如下:

enum ggml_backend_buffer_usage {
    GGML_BACKEND_BUFFER_USAGE_ANY = 0,
    GGML_BACKEND_BUFFER_USAGE_WEIGHTS = 1,
    GGML_BACKEND_BUFFER_USAGE_COMPUTE = 2,
};

其中:

  • GGML_BACKEND_BUFFER_USAGE_ANY:通用缓冲区,可以用于任何用途
  • GGML_BACKEND_BUFFER_USAGE_WEIGHTS:用于存储权重数据的缓冲区
  • GGML_BACKEND_BUFFER_USAGE_COMPUTE:用于计算的缓冲区

buffer 的整个设计的核心思想是将不同的缓冲区操作抽象成接口,通过 ggml_backend_bufferggml_backend_buffer_typeggml_backend_device 等结构体实现对不同硬件(如 CPU 或 GPU)的支持。这样,通过统一的接口和抽象层,框架能够灵活地支持多种设备和后端,同时优化内存管理和数据传输

3.4 ggml_context

context 直译为 “上下文”,在 ggml 框架中会出现各种以 context 为名称的变量,在此框架中我们可以把它翻译为 “环境信息”,类似于操作系统的 “环境变量”(针对不同的程序需要设置不同的环境变量)

对于 ggml 框架来说,无论你要做什么(例如构建 model、建立计算图等)都需要先创建一个 context 作为容器,而你所创建的任何信息结构体(tensor、graph、…)实际都存储在 context 容器包含的地址空间内

下面我们以 ggml_context 为例来讲解,ggml_context 主要用于管理和存储与计算图或张量操作相关的上下文信息,可以承载三种数据类型:Tensor、Graph、Work_buffer。它可以被看作是一个 内存管理资源组织 的中心,负责管理计算中使用的所有对象和内存,内部结构如下图所示:

在这里插入图片描述

Note:图片来自于:llama.cpp源码解读–ggml框架学习

其定义如下:

/* src/ggml.c */

struct ggml_context {
    size_t mem_size;
    void * mem_buffer;
    bool   mem_buffer_owned;
    bool   no_alloc;

    int    n_objects;

    struct ggml_object * objects_begin;
    struct ggml_object * objects_end;
};

其中:

  • mem_size 字段表示 ggml_context 所管理的内存总大小(以字节为单位)
  • mem_buffer 是一个指向内存缓冲区的指针,之前说过 ggml 中一切数据都要放在 buffer 中,这里的 ggml_context 也不例外
  • mem_buffer_owned 字段标识 ggml_context 是否拥有 mem_buffer
    • 如果为 true,意味着 ggml_context 自己负责管理和释放这个内存缓冲区
    • 如果为 false,表示内存由其他部分管理,ggml_context 仅用于访问
  • no_alloc 字段表示 ggml_context 是否允许内存分配保存实际数据
  • n_objects 字段表示 ggml_context 管理的对象数量
  • objects_begin 指向第一个 ggml_object 的指针,标记了管理对象链表的起始位置
  • objects_end 指向最后一个 ggml_object 的指针,它表示当前上下文中对象链表的结束位置。通过这两个指针(objects_beginobjects_end),可以遍历所有的 ggml_object,从而管理所有的计算对象

ggml_object 结构体的定义如下:

enum ggml_object_type {
    GGML_OBJECT_TYPE_TENSOR,
    GGML_OBJECT_TYPE_GRAPH,
    GGML_OBJECT_TYPE_WORK_BUFFER
};

struct ggml_object {
    size_t offs;
    size_t size;

    struct ggml_object * next;

    enum ggml_object_type type;

    char padding[4];
};

ggml_object 代表了 ggml 框架中的一个对象,在 ggml_context 中,每个对象都有以下字段:

  • offs 字段表示对象在 mem_buffer 中的偏移量,也就是说,offs 是指向该对象数据在内存中的位置
  • size 字段表示对象的大小,即该对象所占用的内存大小
  • next 是一个指向下一个 ggml_object 的指针。这种链表结构允许将多个 ggml_object 组织成一个有序的集合,并且可以依次遍历它们
  • type 字段表示对象的类型。它是一个枚举类,定义了几种不同的对象类型,主要有:
    • GGML_OBJECT_TYPE_TENSOR:表示一个张量对象
    • GGML_OBJECT_TYPE_GRAPH:表示一个计算图对象
    • GGML_OBJECT_TYPE_WORK_BUFFER:表示一个工作缓冲区对象
  • padding 数组用于填充,确保结构体的内存对齐

总的来说,ggml_context 类似于一个 “容器” 或 “管理器”,负责保存和组织计算图中的所有对象,它让你可以灵活地处理张量、计算图等对象的内存分配、释放和管理。

3.5 backend

ggml 的后端 backend 有如下几个比较重要的概念:(from here)

  • ggml_backend:执行计算图的接口,有多种类型,包括:CPU(默认)、CUDA、Metal(Apple Silicon)、Vulkan 等等
  • ggml_backend_buffer:表示后端 backend 分配的内存空间
  • ggml_backend_sched:一个调度器,使得多种后端可以并发使用。在处理大模型或多 GPU 推理时,实现跨硬件平台地分配计算任务(如 CPU+GPU 混合计算)。该调度器还能自动将 GPU 不支持的算子转移到 CPU 上,来确保最优的资源利用和兼容性

具体的后端细节与后端调度器内容过多,博主能力有限这边就不分析了,大家感兴趣的可以自己看看相关源码

3.6 ggml_cgraph

计算图(computation graph,cgraph)是 Deep Learning 框架中常见的一个重要概念,用一句话通俗地说:计算图是一种有向图,用来描述计算的依赖关系和执行顺序

它的核心目的是:把复杂的数学表达式拆分成一系列的基本运算节点,然后用图的形式表示这些节点之间的依赖关系

举个例子,假设我们要计算 C = A × B + C C = A \times B + C C=A×B+C,根据 tensor 之间关系构建的计算图如下:

在这里插入图片描述

Note:图片来自于 llama.cpp源码解读–ggml框架学习

在上图中,分两种类型的元素:node 中间节点(白色)和 leaf 叶节点(粉红色)。一切既不是参数(weight)也不是操作数(即 ggml_tensor 结构体中的 op 为 None)的元素称之为 leaf,其余都作为 node(如 op 为 GGML_OP_MUL_MAT

图中 leaf_0leaf_1leaf_2 都是 leaf 类型张量,是输入或常量张量,在计算中不会被修改,其中:

  • leaf_0 (f32) / CONST 0 [4, 3]
    • leaf_0view 的源张量
    • CONST 0 表示它是个常量张量,数据类型为 float32,shape 为 4x3
  • leaf_1 等于 A A A,shape 为 2x4,leaf_2 等于 B B B,shape 为 2x3
  • 注意,ggml 内部会做隐式转换,将 leaf_1 的 shape 做转置变为 4x3

图中 node_0 是 node 类型张量,对应 ggml_tensorop == GGML_OP_MUL_MAT 操作,也就是:

node_0 = ggml_mul_mat(leaf1, leaf2)

最后一个节点 node 有 (view) 标识,结合之前我们对 ggml_tensor 的讲解可知,这个节点是一个对原节点(leaf_0)的引用,即将 A × B + C A \times B +C A×B+C 的计算结果直接存储在 C C C 矩阵的 tensor 中(leaf_0),而不是重新建立一个新的 tensor 来进行存储

ggml 中的计算图结构体 ggml_cgraph 定义如下:

struct ggml_cgraph {
    int size;    // maximum number of nodes/leafs/grads/grad_accs
    int n_nodes; // number of nodes currently in use
    int n_leafs; // number of leafs currently in use

    struct ggml_tensor ** nodes;     // tensors with data that can change if the graph is evaluated
    struct ggml_tensor ** grads;     // the outputs of these tensors are the gradients of the nodes
    struct ggml_tensor ** grad_accs; // accumulators for node gradients
    struct ggml_tensor ** leafs;     // tensors with constant data

    struct ggml_hash_set visited_hash_set;

    enum ggml_cgraph_eval_order order;
};

其中:

  • size 表示计算图中可容纳的最大节点数,包含中间节点、叶节点、梯度和梯度累加器
  • n_nodes 表示当前图中用于计算的 中间节点 数量,比如加法、乘法这些操作形成的节点
  • n_leafs 表示当前图中所有 叶节点 数量
  • nodes 用来存放图中所有中间节点
  • grads 用来存放图中每个计算节点对应的梯度张量
  • grad_accs 用来存放梯度的累加器
  • leafs 用来存放图中的叶子张量
  • visited_hash_set 结构体利用哈希集合来记录哪些节点已经被访问过,用来避免重复处理节点(类似于图遍历中的 “标记是否访问过”)
  • order 字段指定了计算图中节点的评估顺序

总的来说,ggml_cgraph 就是 ggml 中用来组织张量运算顺序、执行依赖、自动微分的计算图引擎核心结构

3.7 ggml_gallocr

我们需要知道,图 graph 是根据 ctx 中 tensor 之间的关系进行建立的,而 graph 中可能包含着输入数据、权重、新建的 node 节点,这些在 graph 中都只是数据的描述信息,而不是数据本身。因此我们还需要有一个专门的内存分配器来对我们计算图 graph 中所需要使用的全部数据进行内存分配,这就是 ggml_gallocr 所要做的事情

ggml_gallocr 定义如下:

/* src/ggml-alloc.c */

struct ggml_gallocr {
    ggml_backend_buffer_type_t * bufts; // [n_buffers]
    ggml_backend_buffer_t * buffers; // [n_buffers]
    struct ggml_dyn_tallocr ** buf_tallocs; // [n_buffers]
    int n_buffers;

    struct ggml_hash_set hash_set;
    struct hash_node * hash_values; // [hash_set.size]

    struct node_alloc * node_allocs; // [n_nodes]
    int n_nodes;

    struct leaf_alloc * leaf_allocs; // [n_leafs]
    int n_leafs;
};

整个结构体分为以下几个部分:(from ChatGPT)

1. 缓冲区管理

  • buftsbuffers
    • bufts:存储了多个后端缓冲区类型(ggml_backend_buffer_type_t),用于描述每个缓冲区的类型信息
    • buffers:存储实际的后端缓冲区(ggml_backend_buffer_t),每个缓冲区代表一块内存区域,供计算图中张量数据存放
    • n_buffers:表示当前管理的缓冲区数量
  • buf_tallocs
    • 每个缓冲区都有对应的动态张量分配器 ggml_dyn_tallocr,用于管理该缓冲区内的内存分配
    • buf_tallocs 是一个指针数组,每个元素对应一个缓冲区内的动态分配器,它负责追踪该缓冲区的空闲内存块、对齐要求以及最大可分配内存量

2. 哈希集合管理

  • hash_sethash_values
    • 在内存分配过程中,可能会遇到重复的分配请求或需要对已有分配做快速查找
    • hash_set 用来记录已经处理过的张量的信息,防止重复分配
    • hash_values 是一个数组,存储每个键对应的 hash_node,其中每个 hash_node 包含了分配状态、所在缓冲区、偏移量以及引用计数信息

3. 节点和叶子分配

  • node_allocsn_nodes
    • 用于记录图中 计算节点(即中间计算结果张量)的内存分配情况
    • 每个 node_alloc 结构体记录了一个节点的输出(dst)内存分配信息以及输入(src)的分配信息
  • leaf_allocsn_leafs
    • 用于记录图中 叶子节点(通常为输入、常量或权重张量)的内存分配情况
    • 每个 leaf_alloc 仅包装了一个 tensor_alloc 结构体,记录了该叶子张量所在的缓冲区、内存偏移和分配的大小

整个 ggml_gallocr(图内存分配器)的设计目的是为了高效管理计算图中各类张量的内存分配,它通过以下几个层面实现这一目标:

  • 缓冲区与动态分配器管理
    • 使用 buffersbufts 以及 buf_tallocs 数组管理底层内存缓冲区,确保内存对齐和空闲块的合理利用
  • 哈希集合辅助配合
    • 通过 hash_sethash_values 快速查找和管理重复分配、共享内存块,防止重复分配及内存浪费
  • 节点与叶子分配
    • 分别使用 node_allocsleaf_allocs 记录计算节点和输入/常量张量的内存分配信息,保证计算图在执行前向传播和反向传播时内存访问正确、布局合理

4. 推理流程整体梳理

在简单了解了 ggml 中的一些核心概念之后,我们先来把 ggml 推理 gpt-2 的总体流程过一遍,也就是先来把 main-backend.cpp 中的 main 函数梳理下,不跳进具体的函数,先把握下推理的整体过程

4.1 时间初始化与参数设置

在这里插入图片描述

在 main 函数中首先我们做了时间初始化与相关参数的一些设置

对于时间初始化,一开始调用了 ggml_time_init() 函数,并记录了程序开始时间,主要是方便后续统计模型加载、模型预测等各阶段的耗时

接着创建了一个 gpt_params 结构体,其定义如下,并将传入的命令参数进行解析并覆盖默认参数,在这里主要传入的是模型的路径、提示词文本等

struct gpt_params {
    int32_t seed         = -1;   // RNG seed
    int32_t n_threads    = std::min(4, (int32_t) std::thread::hardware_concurrency());
    int32_t n_predict    = 200;  // new tokens to predict
    int32_t n_parallel   = 1;    // number of parallel streams
    int32_t n_batch      = 32;   // batch size for prompt processing
    int32_t n_ctx        = 2048; // context size (this is the KV cache max size)
    int32_t n_gpu_layers = 0;    // number of layers to offlload to the GPU

    bool ignore_eos = false; // ignore EOS token when generating text

    // sampling parameters
    int32_t top_k          = 40;
    float   top_p          = 0.9f;
    float   temp           = 0.9f;
    int32_t repeat_last_n  = 64;
    float   repeat_penalty = 1.00f;

    std::string model      = "models/gpt-2-117M/ggml-model.bin"; // model path
    std::string prompt     = "";
    std::string token_test = "";

    bool    interactive      = false;
    int32_t interactive_port = -1;
};

gpt_params 结构体主要用于存储 GPT 模型推理相关的参数,包括批处理大小(n_batch)、上下文窗口大小(n_ctx)、采样参数、提示词文本(prompt)等等

4.2 模型加载与词汇表构建

在这里插入图片描述

参数设置完之后,我们开始利用这些参数来加载我们的模型,并构建词汇表

首先我们定义了 gpt_vocabgpt2_model 两个结构体对象,分别用于存储词汇表和 GPT-2 模型相关的数据,它们的定义如下:

gpt_vocab 结构体定义:

struct gpt_vocab {
    using id    = int32_t;
    using token = std::string;

    std::map<token, id> token_to_id;
    std::map<id, token> id_to_token;
    std::vector<std::string> special_tokens;

    void add_special_token(const std::string & token);
};

gpt_vocab 结构体中 token_to_id 成员变量用于从 token 到 ID 的映射,允许根据 token 查找对应的 ID,例如给定一个 token 如 "hello" 查找它在词汇表中的 ID,而 id_to_token 成员变量则刚好相反,用于从 ID 到 token 的反向映射,special_tokens 则存储着模型中一些特殊的 token 如 eos 等

gpt2_model 结构体定义:

struct gpt2_model {
    gpt2_hparams hparams;

    // normalization
    struct ggml_tensor * ln_f_g;
    struct ggml_tensor * ln_f_b;

    struct ggml_tensor * wte;     // position embedding
    struct ggml_tensor * wpe;     //    token embedding
    struct ggml_tensor * lm_head; // language model head

    std::vector<gpt2_layer> layers;

    // key + value memory
    struct ggml_tensor * memory_k;
    struct ggml_tensor * memory_v;

    //
    struct ggml_context * ctx_w;
    struct ggml_context * ctx_kv;

    ggml_backend_t backend = NULL;

    ggml_backend_buffer_t buffer_w;
    ggml_backend_buffer_t buffer_kv;

    std::map<std::string, struct ggml_tensor *> tensors;
};

gpt2_model 结构体是 GPT-2 模型的核心部分,包含模型的各种参数:

  • hparams 是一个结构体,存储 GPT-2 的超参数,如隐藏层大小、词汇表大小等
  • ln_f_gln_f_b 是 LayerNorm 层的参数,分别表示缩放因子 scale 和偏置项 bias
  • wtewpe 是两个 embedding 向量,分别表示 token 的 embedding 和 position 的 embedding,它们都是 ggml_tensor 结构体
  • lm_head 是语言模型的头,用于计算最终的 logits
  • layers 存储了模型的多个层(通常是 Transformer 层)
  • memory_kmemory_v 是我们常说的 kv cache
  • ctx_wctx_kv 是模型的上下文指针,其中 ctx_w 可能与权重相关,ctx_kv 可能与 kv cache 相关
  • backend 表示模型计算的后端,例如 CPU 或 GPU
  • buffer_wbuffer_kv 用于存储模型计算过程中需要的缓存区
  • tensors 用于存储模型中的各种张量,其中张量名字为键,张量对象为值

接着调用 gpt2_model_load 加载模型文件,同时将定义的 modelvocab 传入,这个函数是我们后续需要重点分析的,该函数包含模型权重加载、ggml 上下文的创建、后端初始化等等工作

4.3 计算图与内存分配

在这里插入图片描述

首先通过 ggml_gallocr_new 创建了一个图形分配器,该分配器基于模型后端默认的 buffer 类型

接着根据当前模型的上下文大小和批处理大小,构建了一个 “最坏情况” 下的计算图(cgraph),它通过 gpt2_graph 函数来构建。然后调用 ggml_gallocr_reserve 根据计算图预留内存,并在终端打印所需的计算缓冲区大小

这个步骤涉及到 ggml 框架的核心数据结构之一:ggml_cgraph(计算图),它用于组织计算步骤和内存管理。还有 ggml_gallocr 作为内存分配器,帮助估算和分配整个推理过程中需要的内存。计算图的构建和内存的分配也是我们后面需要重点分析的

Note:ggml 为了在运行阶段避免动态内存的申请,在计算开始之前会预分配一个 “足够大” 的内存空间。这种做法虽然可以提高运行效率,但也会带来一定的内存浪费,特别是在处理 kv cache 时。相比之下,其他一些推理框架如 vLLM,通过采用 PagedAttention 作为高速缓存管理层,在推理过程中能够更灵活地分配和释放内存。大家如果对 PagedAttention 感兴趣的话,可以看看 vLLM 的 Blog:vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention

4.4 文本预处理与推理过程

模型加载、计算图构建、context 创建、内存分配这些都做完之后就可以正式进入我们的推理环节了

在这里插入图片描述

推理第一步要做的就是 tokenization 分词,对输入的 prompt 使用 gpt_tokenize 函数进行分词,将输入的文本序列转化为对应 token 的 ID 序列。在我们的例子中,输入的 prompt 是 "This is an example",经过分词后得到了四个 token("This""is""an""example") 的 ID

接着调整预测步数,确保整个输入加上预测的 token 数不超过模型上下文的最大长度

在这里插入图片描述

整个推理过程是以 token 为单位逐渐进行的(token-by-token),因此这里我们进入了一个 for 循环。当有 token(embd 不为空)时,调用 gpt2_eval 函数进行前向推理,更新 logits(概率分布)。

我们知道 gpt-2 的 vocabulary 词汇表的大小是 50257,因此这里我们得到了也刚好是 50257 个 logit,它们存储在一个 vector 容器中,每个 logit 是 float 类型的变量

在这里插入图片描述

推理完之后,更新累积已处理的 token 数(n_past),并清空当前的 token 缓冲(embd

根据当前状态,若还处于处理初始 prompt,则按批处理一次性添加后续 token(else 分支,这里未执行)

若已经超过 prompt,则调用 gpt_sample_top_k_top_p 根据 logist 采样(温度采样),得到下一个 token,并将其加入上下文中(embd),在我们的例子中,预测得到的第一个 token 的 id 是 286

在这里插入图片描述

每个生成的 token 都会被立即打印出来,并检查是否遇到结束符以终止生成,在我们的例子中生成的第一个 token 是 "of"

接着模型会根据 "of" 这个 token 预测下一个 token,如此反复循环,直至达到结束生成条件

这个过程是绝大部分 LLM 推理的通用过程,大家如果对 LLM 的推理流程不熟悉,可以看看 理解llama.cpp如何进行LLM推理 这篇文章,这里博主把 LLM 推理过程的流程图贴在下面了:

在这里插入图片描述

4.5 性能统计与资源释放

整个推理完成之后就是一些资源的释放工作了

在这里插入图片描述

在所有生成过程完成后,统计并打印模型加载时间、采样时间、预测时间(以及平均每 token 的预测时间)和总执行时间

最后,依次释放模型使用的资源,包括:

  • 释放模型上下文(ggml_free
  • 释放计算图内存分配器(ggml_gallocr_free
  • 释放后端分配的缓冲区和后端资源(ggml_backend_buffer_freeggml_backend_free

4.6 完整推理流程

OK,以上就是 ggml 推理 gpt-2 的整理流程梳理了

博主绘制了一个简单的流程图来描述这整个过程:

在这里插入图片描述

下面我们就来具体分析其中的每个步骤

Note:由于逐行代码调试展示篇幅占用太大,因此博主下面就只展示一些重点核心部分的函数,例如时间记录、命令行参数解析这类的函数就跳过了,大家如果想要了解更多的细节可以跟随 UP 主的视频一步步调试。另外想要说的是博主这边选取的案例是 gpt-2,UP 主选择的案例是 mnist,其实都大差不差,重点是学习 ggml 中的一些核心概念如 ctx、ggml_tensor、gguf、cgraph 等等,以便更好的学习 llama.cpp

由于篇幅原因,我们将核心部分的函数分析放在下篇文章来讲解😄

结语

这篇文章我们主要学习了利用 ggml 来推理模型(以 gpt-2 为例),同时梳理了 ggml 中的一些核心概念,包括 ggml_tensor、ggml_backend_buffer、ggml_context、ggml_cgraph 等等,大部分的概念解释都来自于 ChatGPT,这里博主对这些概念只是有个简单的印象,具体的设计以及实现博主其实也并不了解,大家可以自己多看看源码实现

OK,以上就是 ggml 推理 gpt-2 总体流程梳理的全部内容了,下篇文章我们就来进入到具体的函数中分析每个步骤都是如何实现的,敬请期待🤗

下载链接

  • ggml源码下载链接【提取码:1234】
  • gpt-2-117M模型下载【提取码:1234】

参考

  • 【大模型部署】GGML源码逐行调试
  • llama.cpp源码解读–ggml框架学习
  • https://github.com/ggml-org/ggml
  • https://chatgpt.com/
  • 理解llama.cpp如何进行LLM推理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2333890.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringCloud-OpenFeign

前言 1.存在问题 远程调用可以像Autowired一样吗 服务之间的通信⽅式,通常有两种:RPC和HTTP. 在SpringCloud中,默认是使⽤HTTP来进⾏微服务的通信,最常⽤的实现形式有两种&#xff1a; RestTemplate OpenFeign RPC&#xff08;RemoteProcedureCall&#xff09;远程过程调⽤&…

撰写学位论文Word图表目录的自动生成

第一步&#xff1a;为图片和表格添加题注 选中图片或表格 右键点击需要编号的图片或表格&#xff0c;选择 【插入题注】&#xff08;或通过菜单栏 引用 → 插入题注&#xff09;。 设置题注标签 在弹窗中选择 标签&#xff08;如默认有“图”“表”&#xff0c;若无需自定义标…

Web 项目实战:构建属于自己的博客系统

目录 项目效果演示 代码 Gitee 地址 1. 准备工作 1.1 建表 1.2 引入 MyBatis-plus 依赖 1.3 配置数据库连接 1.4 项目架构 2. 实体类准备 - pojo 包 2.1 dataobject 包 2.2 request 包 2.3 response 包 2.3.1 统一响应结果类 - Result 2.3.2 用户登录响应类 2.3.3…

【随行付-注册安全分析报告-无验证方式导致隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

什么是原型、原型链?

一、原型 每个函数都有一个prototype属性&#xff0c;称之为原型&#xff0c;也称为原型对象。 原型可以放一些属性和方法&#xff0c;共享给实例对象使用。原型可以用作继承 二、原型链 对象都有_proto_属性&#xff0c;这个属性指向它的原型对象&#xff0c;原型对象也是…

ChatGPT的GPT-4o创建图像Q版人物提示词实例展示

最近感觉GPT-4o发布的新功能真的强大&#xff0c;所以总结了一些提示词分享给大家&#xff0c;大家可以去试试&#xff0c;玩法多多&#xff0c;可以用GPT-4o生成图片&#xff0c;然后用可灵进行图生视频&#xff0c;就能去发布视频了&#xff01;接下来和笔者一起来试试&#…

StringBuffer类基本使用

文章目录 1. 基本介绍2. String VS StringBuffer3. String和StringBuffer相互转换4. StringBuffer类常见方法5. StringBuffer类测试 1. 基本介绍 java.lang.StringBuffer 代表可变的字符序列&#xff0c;可以对字符串内容进行增删很多方法与String相同&#xff0c;但StringBuf…

基于 Maven 构建的 Thingsboard 3.8.1 项目结构

一、生命周期&#xff08;Lifecycle&#xff09; Maven 的生命周期定义了项目构建和部署的各个阶段&#xff0c;图中列出了标准的生命周期阶段&#xff1a; clean&#xff1a;清理项目&#xff0c;删除之前构建生成的临时文件和输出文件。validate&#xff1a;验证项目配置是否…

为啥物联网用MQTT?

前言 都说物联网用MQTT&#xff0c;那分别使用Http和Mqtt发送“Hello”&#xff0c;比较一下就知道啦 HTTP HTTP请求报文由请求行、头部字段和消息体组成。一个最简单的HTTP POST请求如下&#xff1a; POST / HTTP/1.1 Host: example.com Content-Length: 5 Content-Type: …

小甲鱼第004讲:变量和字符串(下)| 课后测试题及答案

问答题: 0. 请问下面代码有没有毛病&#xff0c;为什么? 请问下面代码为什么会出错&#xff0c;应该如何解决&#xff1f; 答:这是由于在字符串中&#xff0c;反斜杠()会与其随后的字符共同构成转义字符。 为了避免这种不测情况的发生&#xff0c;我们可以在字符串的引号前面…

MergeX亮相GTC2025:开启全球广告流量交易新篇章

全球流量盛宴GTC2025深圳启幕&#xff0c;共探出海新蓝海 2025年4月24日至25日&#xff0c;GTC2025全球流量大会将在深圳福田会展中心9号馆隆重召开。作为跨境出海领域内规模最大、资源最丰富、产业链最完备的年度盛会&#xff0c;此次大会将汇聚众多行业精英&#xff0c;共同探…

STM32(基于标准库)

参考博客&#xff1a;江科大STM32笔记 Stm32外设 一、GPIO 基础 GPIO位结构 I/O引脚的保护二极管是对输入电压进行限幅的上面的二极管接VDD, 3.3V,下面接VSS, 0V&#xff0c;当输入电压 >3.3V 那上方这个二极管就会导通&#xff0c;输入电压产生的电流就会大部分充入VD…

国家优青ppt美化_青年科学基金项目B类ppt案例模板

国家优青 国家优青&#xff0c;全称“国家优秀青年基金获得者”。2025改名青年科学基金B类。 作为自然基金人才资助类型&#xff0c;支持青年学者在基础研究方面自主选择研究方向开展创新研究。它是通往更高层次科研荣誉的重要阶梯&#xff0c;是准杰青梯队。 / WordinPPT /…

解决 ECharts 图表无数据显示问题

问题&#xff1a; 在开发项目时&#xff0c;后端明明已经成功返回了数据&#xff0c;但在展示手账发布数量趋势和树洞帖子发布数量趋势的 ECharts 图表中&#xff0c;却只有坐标轴&#xff0c;没有任何数据显示。 以我的VUE项目开发可视化面板为例&#xff0c;下面将详细分析可…

spacy安装失败报错

报错 使用命令pip install spacy安装spacy时总是报错&#xff08;python -m pip install spacy方式安装同样报错&#xff09; 解决办法 使用conda安装&#xff0c;conda能够避免很多不必要的依赖包。 命令&#xff1a;conda install spacy 安装成功列表展示

C++在Linux上生成动态库并调用接口测试

加减乘除demo代码 项目结构 CPP/ ├── calculator.cpp ├── calculator.h ├── main.cpp 头文件 #ifndef CALCULATOR_H #define CALCULATOR_H#ifdef __cplusplus extern "C" {#endifdouble add(double a, double b);double subtract(double a, double b…

前端性能测试工具 —— WebPageTest

测试工具介绍 WebPageTest 是一个用于测量和分析网页性能的工具。它提供了详细的诊断信息&#xff0c;帮助用户了解网页在不同条件下的表现。用户可以选择全球不同的测试地点&#xff0c;使用真实的浏览器&#xff0c;并自定义网络条件进行测试。WebPageTest 还支持核心网络指…

北邮LLMs在导航中的应用与挑战!大模型在具身导航中的应用进展综述

作者&#xff1a;Jinzhou Lin, Han Gao, Xuxiang Feng, Rongtao Xu, Changwei Wang, Man Zhang, Li Guo, Shibiao Xu 单位&#xff1a;北京邮电大学人工智能学院&#xff0c;中国科学院自动化研究所多模态人工智能系统国家重点实验室&#xff0c;中科院空间信息研究所&#xf…

Windows下ElasticSearch8.x的安装步骤

下载ElasticSearch&#xff1a;https://www.elastic.co/downloads/elasticsearch &#xff08;我下载的是目前最新版8.17.4&#xff09;解压ElasticSearch 进入到ElasticSearch的bin目录下双击elasticsearch.bat 弹出控制台并开始执行&#xff0c;在这一步会输出初始账号和密码…

【高性能缓存Redis_中间件】一、快速上手redis缓存中间件

一、铺垫 在当今的软件开发领域&#xff0c;消息队列扮演着至关重要的角色。它能够帮助我们实现系统的异步处理、流量削峰以及系统解耦等功能&#xff0c;从而提升系统的性能和可维护性。Redis 作为一款高性能的键值对数据库&#xff0c;不仅提供了丰富的数据结构&#xff0c;…