llama.cpp 分布式推理介绍(6) 张量序列化 (rpc_tensor)

在上一章 RPC 通信协议](05_rpc_通信协议_.md 中,我们学习了客户端和服务器之间沟通的"语言"规则。我们知道,当客户端要执行一个复杂的计算图时,它会发送一个 RPC_CMD_GRAPH_COMPUTE 命令,并附带上计算图的数据。

但是,这里有一个核心问题:计算图是由许多 ggml_tensor(张量)组成的,而 ggml_tensor 是一个 C 语言的结构体,它包含了大量的内存指针。我们都知道,内存指针是不能直接通过网络发送的 ,因为客户端内存地址 0x1234ABCD 在服务器上是毫无意义的。

那么,ggml-rpc 是如何将一个复杂的、包含指针的 ggml_tensor 结构,"翻译"成适合在网络上传输的格式呢?这就是本章的主角------rpc_tensor 要解决的问题。

1. 问题的提出:我的"快递"信息不全

想象一下,你想把一个贵重的花瓶(ggml_tensor)从北京寄到上海。这个花瓶有它自身的属性(比如材质、尺寸、重量),并且它在你家里的具体位置(比如客厅架子的第二层)。

如果你直接打电话给上海的朋友说:"请到我家客厅架子第二层来取花瓶",这显然是行不通的。你需要一个标准化的流程:

  1. 打包花瓶:将花瓶的数据(像素、权重等)准备好。
  2. 填写快递单:在一张标准表格上,写清楚花瓶的描述信息(尺寸、重量、类型),并指明它应该被送到上海仓库的哪个货架上。这张快递单不能包含"我家客厅"这种只有你自己才懂的信息。
  3. 寄送:将花瓶和快递单一起交给快递公司。

ggml-rpc 中,内存中的 ggml_tensor 就像是那个在你家里的花瓶,它包含的内存指针(如 tensor->data, tensor->buffer)就是"我家客厅架子"这样的本地信息。为了通过网络(快递公司)把它发送给服务器(上海的朋友),我们需要一张标准化的"快递单"。这张快递单就是 rpc_tensor

2. 核心概念:标准化的"快递单"

rpc_tensor 是一种用于在网络上传输张量元数据的"打包格式 "或"序列化结构"。

它就像一张标准化的"快递单",将一个内存中的 ggml_tensor 的所有关键信息,转换成一个不包含任何本地内存指针的、紧凑的、适合网络传输的表单。

rpc_tensor 的核心任务是:用"通用语言"描述一个张量,替代掉 ggml_tensor 中那些只能在本地理解的"方言"(内存指针)。

当服务器收到这张"快递单"后,它就能根据上面的信息,在自己的内存中准确地重建出这个张量的结构,并知道应该从哪里获取它的数据。

rpc_tensor 里有什么?

让我们打开这张"快递单",看看里面都填写了哪些关键信息。rpc_tensor 是一个定义在 ggml-rpc.cpp 中的 C 结构体。

c 复制代码
// 文件: ggml-rpc.cpp

#pragma pack(push, 1)
struct rpc_tensor {
    uint64_t id;         // 张量的唯一标识符 (像快递追踪号)
    uint32_t type;       // 类型 (如 FP16, INT8)
    uint32_t ne[GGML_MAX_DIMS]; // 维度/形状 (如 1x512)
    uint32_t nb[GGML_MAX_DIMS]; // 步长信息

    uint64_t buffer;     // **关键**: 远程缓冲区的标识符
    uint64_t data;       // **关键**: 在远程缓冲区中的偏移量

    uint64_t src[GGML_MAX_SRC]; // 来源张量的标识符
    // ... 其他字段 ...
    char name[GGML_MAX_NAME];   // 张量的名字
};
#pragma pack(pop)

我们来解读一下最重要的几个字段:

  • id: 这是一个 64 位的数字,唯一地标识了这个张量对象。它就像是快递的追踪号,让服务器可以在众多张量中准确地找到它。这个 id 通常就是原始 ggml_tensor 的内存地址,但在网络传输中,它只被当作一个独一无二的数字ID使用。
  • type, ne, nb, name: 这些都是张量的基本元数据,描述了"这个包裹里是什么",比如它的数据类型、形状等。这些信息被原封不动地从 ggml_tensor 复制过来。
  • buffer: 这是最重要的字段之一 。它不再是一个本地内存指针,而是一个代表远程服务器上 的远程后端缓冲区 (RPC Buffer)的标识符。它告诉服务器:"这个张量的数据,存放在你的第 N 号仓库里"。
  • data: 这也是一个关键字段 。它也不是一个内存指针,而是数据在上述 buffer(远程仓库)中的偏移量(offset)。它告诉服务器:"包裹在 N 号仓库的第 M 号货架上"。
  • src: 计算图中的张量之间存在依赖关系(比如 c = a + b)。这个字段存储的不是 src 张量的内存指针,而是它们的 id(追踪号)。这样,服务器在收到所有"快递单"后,就可以根据这些 id 重新建立起张量之间的连接。

通过这种方式,rpc_tensor 成功地将所有与本地内存布局相关的信息都替换成了通用的、基于标识符的引用。

3. 如何使用:填写"快递单"的过程

我们作为用户,通常不需要直接创建 rpc_tensor。这个"填写快递单"的过程是由 ggml-rpc 内部自动完成的。这个过程的专业术语叫做"序列化 (Serialization)"。

ggml-rpc 提供了一个内部函数 serialize_tensor 来完成这个转换。

c 复制代码
// 文件: ggml-rpc.cpp

static rpc_tensor serialize_tensor(const ggml_tensor * tensor) {
    rpc_tensor result;

    // 1. 将指针地址转换为唯一的 ID
    result.id = reinterpret_cast<uint64_t>(tensor);

    // 2. 复制基本元数据
    result.type = tensor->type;
    for (uint32_t i = 0; i < GGML_MAX_DIMS; i++) {
        result.ne[i] = tensor->ne[i];
        result.nb[i] = tensor->nb[i];
    }

    // 3. 将 buffer 指针转换为远程 buffer 的 ID
    if (tensor->buffer) {
        ggml_backend_rpc_buffer_context * ctx = (ggml_backend_rpc_buffer_context *)tensor->buffer->context;
        result.buffer = ctx->remote_ptr; // 使用远程指针 ID
    }

    // 4. 复制数据偏移量 (这本身就是一个相对值,可以直接使用)
    result.data = reinterpret_cast<uint64_t>(tensor->data);

    // 5. 将来源张量的指针转换为它们的 ID
    for (uint32_t i = 0; i < GGML_MAX_SRC; i++) {
        result.src[i] = reinterpret_cast<uint64_t>(tensor->src[i]);
    }

    // ... 复制其他字段 ...
    return result;
}

这段代码清晰地展示了"填写快递单"的每一个步骤:将所有本地指针(tensor, tensor->buffer, tensor->src)都转换成 uint64_t 类型的标识符,并将其他元数据直接复制。

4. 幕后探秘:一个计算图的序列化之旅

现在我们知道了单个张量是如何被序列化的。那么当客户端调用 ggml_backend_graph_compute 时,整个计算图是如何被打包发送的呢?

高层流程

  1. 遍历计算图ggml_backend_rpc_graph_compute 函数首先会遍历计算图(cgraph)中的所有节点(ggml_tensor)。
  2. 创建"快递单" :对于每一个遇到的 ggml_tensor,它都会调用 serialize_tensor 函数,为之生成一个对应的 rpc_tensor "快递单"。
  3. 收集所有"快递单" :它会将所有生成的 rpc_tensor 收集到一个列表中。
  4. 打包发送 :最后,它将这个 rpc_tensor 列表,连同计算图的节点顺序信息,一起打包成一个大的二进制消息体,通过网络发送给服务器。

这个过程可以用一个时序图来表示:

sequenceDiagram participant 客户端应用 participant RPC后端 participant RPC服务器 客户端应用->>RPC后端: 调用 ggml_backend_graph_compute(cgraph) activate RPC后端 Note right of RPC后端: 开始序列化计算图... loop 遍历 cgraph 中的每个 ggml_tensor RPC后端->>RPC后端: 调用 serialize_tensor(tensor) Note right of RPC后端: 生成一个 rpc_tensor (快递单) end Note right of RPC后端: 将所有 rpc_tensor 打包成一个消息 RPC后端->>RPC服务器: 发送 COMPUTE_GRAPH 请求 (携带所有 rpc_tensor) activate RPC服务器 RPC服务器->>RPC服务器: 接收并反序列化,重建计算图 deactivate RPC服务器 deactivate RPC后端

服务器端的"收货"过程

当服务器收到这个包含大量 rpc_tensor 的消息后,它会执行一个逆向操作------反序列化 (Deserialization)

  1. 解析消息 :服务器首先解析出 rpc_tensor 列表和节点信息。
  2. 重建张量 :它会遍历 rpc_tensor 列表。对于每一个 rpc_tensor,它会在服务器的内存中创建一个新的 ggml_tensor,并将 rpc_tensor 中的元数据(类型、维度等)复制过去。
  3. 链接关系 :最关键的一步是,服务器会使用 rpc_tensor 中的 id, buffer, src 等标识符,在服务器端正确地恢复张量之间的依赖关系和它们在远程缓冲区中的位置。
  4. 得到完整的计算图 :当所有 rpc_tensor 都被处理完毕后,服务器就在自己的内存中拥有了一个与客户端结构完全相同、但所有指针都指向服务器本地资源的 ggml_cgraph 副本。之后,它就可以将这个计算图交给本地的 CUDA 或 Metal 后端去执行了。

5. 总结与展望

在本章中,我们深入探讨了 ggml-rpc 实现远程计算的关键一步------张量序列化

  • 我们理解了为什么内存中的 ggml_tensor 不能直接通过网络发送,因为它包含了本地内存指针。
  • 我们认识了 rpc_tensor 这个核心数据结构,它就像一张标准化的"快递单 ",用不含指针的、基于标识符的方式描述了一个张量的所有元信息。
  • 我们了解了客户端通过序列化 过程将 ggml_tensor 转换为 rpc_tensor,以及服务器通过反序列化过程将其还原的机制。

通过 rpc_tensor,我们解决了如何向服务器描述"什么东西"和"它们之间的关系"的问题。我们反复提到,rpc_tensor 中的 bufferdata 字段指向了服务器上的一个"远程仓库"和"货架"。

这个"远程仓库"到底是什么?客户端是如何请求服务器创建一个仓库,又如何管理它呢?这正是我们下一章要揭晓的答案。

相关推荐
山烛10 分钟前
OpenCV图像形态学操作
图像处理·人工智能·python·opencv·计算机视觉·图像形态学
向左转, 向右走ˉ13 分钟前
神经网络显存占用分析:从原理到优化的实战指南
人工智能·深度学习·神经网络
掘金安东尼1 小时前
数据仓库现代化迁移到亚马逊 Redshift 完整指南
人工智能
掘金安东尼1 小时前
Amazon Polly :让文字开口说话的云端实践
人工智能·云原生
后端小肥肠1 小时前
从 0 到 1 用 Coze 做美食漫画,长尾流量 + 长期收益全拿下,小白可学!
人工智能·aigc·coze
机器之心2 小时前
好莱坞特效师展示AI生成的中文科幻大片,成本只有330元
人工智能·openai
Codebee2 小时前
用原生AI-IDE快速搞定OneCode视图注解:AI与注解驱动开发的完美结合
人工智能·低代码
aneasystone本尊2 小时前
GraphRAG 快速入门
人工智能
用户5191495848452 小时前
TypeScript Record类型完全指南:从基础到高级应用
人工智能·aigc
听风.8252 小时前
机器学习6
人工智能·机器学习·概率论