llama.cpp 分布式推理介绍(3) 远程过程调用后端 (RPC Backend)

在上一章中,我们学习了后端注册机制 (Backend Registration),了解了 GGML 框架如何像发现插件一样,知道 "RPC" 这种后端类型的存在。结合第一章 远程计算设备 (RPC Device)的知识,我们现在已经能在本地创建一个指向远程服务器的"快捷方式"了。

但光有"快捷方式"还不够。我们如何通过这个快捷方式,真正地向远程服务器下达指令,比如"请帮我分配一块 GPU 显存"或者"请帮我运行这个复杂的计算任务"呢?

这时,我们就需要一个功能更强大的工具------一个"遥控器"。这个"遥控器"就是本章的主角:远程过程调用后端 (RPC Backend)

1. 问题的提出:我需要一个"遥控器"

回到我们最初的场景:你的笔记本电脑算力不足,而远处有一台强大的服务器。

  • 通过远程计算设备 (RPC Device),我们创建了一个"快捷方式",让我们的应用程序知道了这台服务器的存在。
  • 通过后端注册机制 (Backend Registration),GGML 框架管理并识别了这种"远程"类型的设备。

现在,我们面临着最关键的一步:如何使用它?我们需要一个能与远程服务器实时通信、发送具体指令并接收结果的实体。这个实体不仅要能建立连接,还要能理解"计算图"这样的高级概念,并将其翻译成网络另一端的服务器能懂的语言。

这正是 RPC Backend 的职责。它充当了客户端与服务器之间的"总指挥"和"翻译官"。

2. 核心概念:什么是 RPC 后端?

远程过程调用后端 (RPC Backend) 是客户端的核心抽象,代表了与远程计算服务器的一个活动连接和指令通道

如果说 RPC Device 是一个静态的"快捷方式",那么 RPC Backend 就是一个动态的"遥控器"。

当你拿起这个"遥控器"(创建 RPC Backend 实例),你就建立了一个与远程服务器的专属通信会话。通过按下遥控器上的按钮(调用后端函数),你可以命令服务器执行各种操作。

这个"遥控器"主要负责以下几项工作:

  1. 管理连接:它持有与服务器的底层网络套接字(socket)连接,负责所有的数据收发。
  2. 执行命令 :它提供了一系列标准化的函数接口,让你能像调用本地函数一样,触发远程操作。最重要的操作就是 graph_compute(执行计算图)。
  3. 序列化与反序列化 :当你命令它"执行这个计算图"时,它会负责将本地内存中的计算图结构(ggml_cgraph)打包成二进制数据流(序列化),通过网络发送给服务器。这个过程对用户是完全透明的。
  4. 隐藏网络细节 :你完全不需要关心 TCP/IP 协议、端口号、数据包格式等底层网络细节。所有这些都被 RPC Backend 优雅地封装好了。

总而言之,RPC Backend 是你与远程算力进行交互的唯一入口

3. 如何使用:创建并连接到 RPC 后端

ggml-rpc 提供了一个非常直观的函数来创建这个"遥控器":ggml_backend_rpc_init

假设远程服务器地址依然是 192.168.1.100:18080

c 复制代码
#include "ggml-rpc.h"
#include <stdio.h>

int main() {
    const char * endpoint = "192.168.1.100:18080";

    // 初始化 RPC 后端,获取一个"遥控器"句柄
    ggml_backend_t rpc_backend = ggml_backend_rpc_init(endpoint);

    if (rpc_backend) {
        // 使用标准的 GGML 函数获取后端名称
        printf("成功创建 RPC 后端,连接到: %s\n", ggml_backend_get_name(rpc_backend));

        // 在这里,我们就可以使用 rpc_backend 来分配远程内存、执行计算了
        // ...

        // 使用完毕后,释放后端资源
        ggml_backend_free(rpc_backend);
        printf("RPC 后端已释放。\n");
    } else {
        printf("创建 RPC 后端失败。\n");
    }
    return 0;
}

代码解释:

  • ggml_backend_rpc_init(endpoint) 函数接收服务器地址作为参数,并返回一个 ggml_backend_t 类型的句柄。这个句柄就是我们所说的"遥控器"。
  • 我们可以对这个句柄使用 GGML 的标准后端函数,例如 ggml_backend_get_name,来获取它的信息。
  • 使用完毕后,务必调用 ggml_backend_free 来释放资源,这会隐式地断开与服务器的连接。

预期输出:

ini 复制代码
成功创建 RPC 后端,连接到: RPC[192.168.1.100:18080]
RPC 后端已释放。

拿到 rpc_backend 句柄后,你就拥有了指挥远程服务器的能力。虽然我们在这个简单的例子里没有执行实际的计算,但创建这个后端实例是所有后续操作的前提。

4. 幕后探秘:init 时发生了什么?

你可能会好奇,调用 ggml_backend_rpc_init 时,是不是立刻就和服务器建立了 TCP 连接?答案是:不一定

和创建 RPC Device 类似,RPC Backend 的初始化也遵循"延迟连接"或"按需连接"的策略,以提高效率。

高层流程

  1. 调用 ggml_backend_rpc_init
    • 这个函数并不会立即建立网络连接
    • 它首先在内存中创建一个 ggml_backend 结构体,这个结构体就是返回给你的句柄。
    • 它在内部创建了一个 ggml_backend_rpc_context,用于保存服务器的地址 endpoint 等信息。
    • 最关键的一步是,它将一组特殊的 RPC 接口函数 (ggml_backend_rpc_interface) 关联到了这个 ggml_backend 结构体上。这些函数(如 ggml_backend_rpc_graph_compute)才是真正知道如何与服务器通信的实现。
  2. 首次执行需要通信的操作 (例如 ggml_backend_graph_compute):
    • 当你第一次调用一个需要与服务器交互的函数时,ggml-rpc 客户端会检查是否存在一个到该服务器的活动连接。
    • 如果不存在,它会此时才尝试与服务器建立网络连接。
    • 连接成功后,它会将你的指令(例如,序列化后的计算图)发送出去。
    • 等待服务器响应,并将结果返回给你。

让我们用一个时序图来清晰地展示这个过程:

sequenceDiagram participant 应用程序 participant ggml-rpc客户端 participant ggml-rpc服务端 应用程序->>ggml-rpc客户端: 调用 ggml_backend_rpc_init("ip:port") activate ggml-rpc客户端 Note right of ggml-rpc客户端: 1. 创建后端上下文 (存储 endpoint)
2. 关联 RPC 接口函数
3. **此时不建立网络连接** ggml-rpc客户端-->>应用程序: 返回后端句柄 (ggml_backend_t) deactivate ggml-rpc客户端 应用程序->>ggml-rpc客户端: 调用 ggml_backend_graph_compute(后端句柄, cgraph) activate ggml-rpc客户端 Note right of ggml-rpc客户端: 这是第一个需要通信的命令 ggml-rpc客户端->>ggml-rpc服务端: 建立网络连接 (按需首次连接) activate ggml-rpc服务端 ggml-rpc客户端->>ggml-rpc服务端: 序列化 cgraph 并发送 COMPUTE_GRAPH 请求 ggml-rpc服务端->>ggml-rpc服务端: 在远程 GPU 上执行计算 ggml-rpc服务端-->>ggml-rpc客户端: 响应:返回计算状态 deactivate ggml-rpc服务端 ggml-rpc客户端-->>应用程序: 返回计算状态 deactivate ggml-rpc客户端

深入代码

让我们深入 ggml-rpc.cpp 源码,看看这一切是如何实现的。

首先是 ggml_backend_rpc_init 函数:

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

ggml_backend_t ggml_backend_rpc_init(const char * endpoint) {
    // 1. 创建上下文,存储 endpoint 和名称
    ggml_backend_rpc_context * ctx = new ggml_backend_rpc_context {
        /* .endpoint  = */ endpoint,
        /* .name      = */ "RPC[" + std::string(endpoint) + "]",
    };

    // 2. 创建后端结构体,并关联 RPC 的接口函数
    ggml_backend_t backend = new ggml_backend {
        /* .guid    = */ ggml_backend_rpc_guid(), // RPC 后端的唯一标识符
        /* .iface   = */ ggml_backend_rpc_interface, // 核心!关联操作函数
        /* .device  = */ ggml_backend_rpc_add_device(endpoint),
        /* .context = */ ctx
    };
    return backend;
}

如我们所分析的,这段代码的核心就是创建结构体和上下文,并将 .iface 字段指向 ggml_backend_rpc_interface。这里没有任何网络操作。

ggml_backend_rpc_interface 是什么呢?它是一个包含了所有后端操作函数指针的结构体:

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

static ggml_backend_i ggml_backend_rpc_interface = {
    /* .get_name                = */ ggml_backend_rpc_name,
    /* .free                    = */ ggml_backend_rpc_free,
    // ... 其他函数指针 ...
    /* .graph_compute           = */ ggml_backend_rpc_graph_compute, // 关键的计算函数
    // ... 其他函数指针 ...
};

当我们调用 ggml_backend_graph_compute(rpc_backend, ...) 时,GGML 框架实际上会通过 rpc_backend->iface.graph_compute(...) 调用到我们上面指定的 ggml_backend_rpc_graph_compute 函数。

我们再来看看这个函数的简化版实现,看看网络通信是如何被触发的:

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

static enum ggml_status ggml_backend_rpc_graph_compute(ggml_backend_t backend, ggml_cgraph * cgraph) {
    ggml_backend_rpc_context * rpc_ctx = (ggml_backend_rpc_context *)backend->context;

    // 1. 获取到服务器的 socket 连接 (如果不存在则创建)
    auto sock = get_socket(rpc_ctx->endpoint);

    // 2. 将计算图 cgraph 序列化成字节流
    std::vector<uint8_t> input;
    serialize_graph(cgraph, input);

    // 3. 通过 socket 发送 RPC 命令和数据
    rpc_msg_graph_compute_rsp response;
    bool status = send_rpc_cmd(sock, RPC_CMD_GRAPH_COMPUTE, input.data(), input.size(), &response, sizeof(response));
    
    // ... 处理响应 ...
    return (enum ggml_status)response.result;
}

这下就非常清楚了!真正的网络操作发生在具体的命令函数内部:获取连接 -> 打包数据 -> 发送命令。这种设计将职责清晰地分离开来,使得整个系统既高效又易于维护。

5. 总结与展望

在本章中,我们认识了 ggml-rpc 的核心交互工具------远程过程调用后端 (RPC Backend)

  • 我们理解了它就像一个与远程服务器通信的"遥控器",是执行所有远程操作的入口。
  • 我们学会了使用 ggml_backend_rpc_init 来创建一个 RPC Backend 实例,为后续的计算任务做好准备。
  • 我们还深入探究了其"按需连接"的内部机制,明白了 init 和实际的网络通信是分离的,具体命令的执行才会触发数据交换。

到目前为止,我们已经完整地学习了 ggml-rpc 客户端的几个核心组件。我们知道如何注册后端类型,如何创建设备代理,以及如何初始化后端"遥控器"来发送指令。

但是,这个"遥控器"发出的信号,究竟被谁接收了呢?网络另一端的服务器是如何监听这些请求,并调用真正的 GPU 来完成计算的?

是时候揭开服务器端的神秘面纱了。下一章,我们将一同探索 ggml-rpc 的另一半------那个在远方默默等待指令的强大引擎。

相关推荐
智能汽车人5 分钟前
行业分析---领跑汽车2025第二季度财报
人工智能·microsoft
先做个垃圾出来………15 分钟前
迁移学习(Transfer Learning)
人工智能·机器学习·迁移学习
许泽宇的技术分享17 分钟前
ReAct Agent:让AI像人类一样思考与行动的革命性框架
人工智能·agent·react
long_run1 小时前
C++之auto 关键字
c++
eBest数字化转型方案1 小时前
2025年快消品行业渠道数字化营销系统全景透视与选型策略
人工智能
kkcodeer1 小时前
大模型Prompt原理、编写原则与技巧以及衡量方法
人工智能·prompt·ai大模型
疯狂的代M夫1 小时前
C++对象的内存布局
开发语言·c++
DevSecOps选型指南2 小时前
SBOM风险预警 | NPM前端框架 javaxscript 遭受投毒窃取浏览器cookie
前端·人工智能·前端框架·npm·软件供应链安全厂商·软件供应链安全工具
rocksun2 小时前
MCP利用流式HTTP实现实时AI工具交互
人工智能·mcp