llama.cpp 分布式推理介绍(4) RPC 服务器 (rpc_server)

在前一章 远程过程调用后端 (RPC Backend) 中,我们学习了客户端的"遥控器"------RPC Backend。我们知道了如何创建它来向远方发送指令。但是,这个遥控器按下的信号,究竟是被谁接收和执行的呢?

如果说客户端的 RPC Backend 是一个遥控器,那么必然需要一台"电视机"来接收信号并播放画面。这台"电视机"就是我们本章的主角------RPC 服务器 (rpc_server) 。它是整个 ggml-rpc 架构中负责干"脏活累活"的幕后英雄。

1. 问题的提出:谁在远方听我号令?

让我们回到最初的场景。你在笔记本上运行一个程序,通过 RPC Backend 发送了一个"计算这个神经网络"的指令。这条指令通过网络飞向了远方的高性能服务器。

那么,在这台服务器上,到底发生了什么?

  • 必须有一个程序始终在运行,像一个忠诚的管家,随时准备接收来自网络的请求。
  • 这个程序必须能听懂我们客户端发来的"语言"(即通信协议)。
  • 当它收到"计算神经网络"的指令时,它必须知道如何调用服务器上真正的硬件资源(比如 NVIDIA GPU)来完成这个任务。
  • 完成任务后,它还要把结果(比如"计算成功"或"计算失败")打包好,再发回给我们的笔记本电脑。

这个在服务器上默默等待、接收、执行并响应的程序,就是 rpc_server。没有它,我们客户端的"遥d控器"就毫无用处。

2. 核心概念:什么是 RPC 服务器?

RPC 服务器 (rpc_server) 是运行在远程机器上的核心服务程序,是整个 RPC 架构的"执行者"。

它就像一个随时待命的"超级管家",不知疲倦地监听着来自客户端的请求。

想象一下,你在一个大型仓库里,用对讲机(客户端 RPC Backend)下达指令:"请把 A 区的 3 号货物搬到 B 区"。仓库里有一位拿着对讲机、配备了叉车(GPU)的超级搬运工(rpc_server)。他听到指令后,会立刻开动叉车,精准地完成搬运工作,然后通过对讲机回复你:"任务完成"。

这位"超级管家"或"搬运工"的核心职责包括:

  1. 监听端口 :它会"竖起耳朵",监听服务器上一个特定的网络端口(例如 18080),等待客户端前来"敲门"(建立连接)。
  2. 解析命令:一旦有客户端连接上,它就能解析客户端发来的二进制数据流,理解其意图,比如"分配显存"、"传输张量数据"或"执行计算图"。
  3. 调用本地后端 :这是它最核心的功能。它会将客户端的请求转换 为对服务器本地 GGML 后端(例如 ggml-backend-cuda)的调用。所有繁重的计算都由这个本地后端在服务器的 GPU 上完成。
  4. 返回结果:执行完毕后,它将执行结果(成功、失败或具体数据)打包,通过网络连接返回给客户端。

简而言之,rpc_server 是连接客户端虚拟指令和服务器真实算力之间的关键桥梁。

3. 如何使用:启动并运行服务器

与客户端的代码库不同,rpc_server 通常是一个可以直接运行的可执行程序。你需要在你的高性能服务器上,通过命令行来启动它。

假设你的服务器有一块 NVIDIA GPU,并且你希望 rpc_server 在所有网络接口的 18080 端口上监听,并使用 CUDA 后端来处理计算任务。

你需要在服务器的终端中输入以下命令:

bash 复制代码
./rpc_server --host 0.0.0.0 --port 18080 --backend CUDA

命令参数解释:

  • --host 0.0.0.0: 0.0.0.0 是一个特殊的 IP 地址,表示服务器将在其所有可用的网络接口上进行监听。这使得局域网或公网上的客户端都能连接到它。
  • --port 18080: 指定了服务器监听的端口号。客户端在连接时必须使用相同的端口号。
  • --backend CUDA: 这是非常关键的参数!它告诉 rpc_server,当收到计算任务时,应该使用本地的 CUDA 后端 来执行。如果你是在一台 Apple Silicon Mac 上,你可能会使用 --backend Metal

启动后的预期输出:

当你执行上述命令后,你会在服务器的终端上看到类似下面的日志信息,这表示服务器已经成功启动并进入等待状态:

yaml 复制代码
Starting RPC server v0.2.0
  endpoint       : 0.0.0.0:18080
  local cache    : n/a
  backend memory : 22874 MB
Accepted client connection, free_mem=22874, total_mem=24576

现在,这个"超级管家"已经准备就绪了。只要它在运行,你的客户端程序就可以随时连接上来,将计算任务卸载到这台服务器上。

4. 幕后探秘:一个请求的生命周期

当客户端调用 ggml_backend_graph_compute 时,服务器内部究竟发生了怎样一番天翻地覆的变化呢?让我们来追踪一个请求的完整生命周期。

高层流程

  1. 启动与监听rpc_server 程序启动,初始化指定的本地后端(例如 CUDA),然后在指定的端口上创建一个服务器套接字(socket),并调用 listen() 开始等待客户端连接。
  2. 接受连接 :当我们的客户端程序(在前几章中编写的)尝试连接时,服务器通过 accept() 函数接受连接,为这个客户端创建一个专属的通信通道(一个新的 socket)。
  3. 进入命令循环 :服务器为这个客户端启动一个循环,不断地从这个专属通道中读取数据 (recv_data)。
  4. 接收和分发命令 :服务器首先读取一个字节来确定命令类型(例如 RPC_CMD_GRAPH_COMPUTE),然后读取后续的数据(例如序列化后的计算图)。接着,它会使用一个巨大的 switch 语句,将请求"分发"给对应的处理函数,比如 server.graph_compute()
  5. 执行本地计算graph_compute 函数会首先反序列化 接收到的数据,在服务器的内存中重建出 ggml_cgraph 结构。然后,它会调用它在启动时初始化的本地 CUDA 后端,执行 ggml_backend_graph_compute(cuda_backend, graph)。这时,服务器的 GPU 开始真正地"火力全开"。
  6. 发送响应 :GPU 计算完成后,本地 CUDA 后端返回一个状态。rpc_server 将这个状态打包成一个响应消息,通过专属通道发回 (send_msg) 给客户端。
  7. 继续等待:服务器处理完一个请求后,并不会关闭连接,而是回到第 3 步,继续等待这个客户端的下一个命令。

这个过程可以用一个时序图清晰地展示出来:

sequenceDiagram participant 客户端 participant RPC 服务器 participant 本地 CUDA 后端 Note over RPC 服务器: 服务器已启动并监听端口 客户端->>RPC 服务器: 发起 TCP 连接 activate RPC 服务器 RPC 服务器-->>客户端: 接受连接 客户端->>RPC 服务器: 发送 COMPUTE_GRAPH 请求 (携带序列化的计算图) RPC 服务器->>RPC 服务器: 反序列化数据,重建计算图 RPC 服务器->>本地 CUDA 后端: 调用 ggml_backend_graph_compute(graph) activate 本地 CUDA 后端 Note over 本地 CUDA 后端: 在 GPU 上执行实际计算 本地 CUDA 后端-->>RPC 服务器: 返回计算状态 (例如:成功) deactivate 本地 CUDA 后端 RPC 服务器-->>客户端: 发送响应消息 (包含计算状态) deactivate RPC 服务器

深入代码

让我们深入 ggml-rpc.cpp 的源码,看看这些步骤是如何在代码中体现的。

首先是服务器的启动和主循环,位于 ggml_backend_rpc_start_server 函数中:

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

void ggml_backend_rpc_start_server(...) {
    // ... (初始化网络) ...

    // 1. 创建服务器套接字并开始监听
    auto server_socket = create_server_socket(host.c.str(), port);
    // ... (错误处理) ...

    while (true) {
        // 2. 接受一个新的客户端连接
        auto client_socket = socket_accept(server_socket->fd);
        // ... (错误处理) ...
        printf("Accepted client connection\n");

        // 3. 为这个客户端处理所有请求
        rpc_serve_client(backend, ..., client_socket->fd, ...);

        printf("Client connection closed\n");
    }
    // ...
}

这段代码清晰地展示了服务器如何循环等待并接受客户端连接,然后将每个连接交给 rpc_serve_client 函数处理。

rpc_serve_client 函数是真正的命令处理器,它包含了一个 while 循环和 switch 语句:

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

static void rpc_serve_client(...) {
    rpc_server server(backend, cache_dir);
    // ... (处理初始的 HELLO 握手) ...

    while (true) {
        // 4. 从客户端读取一个命令字节
        if (!recv_data(sockfd, &cmd, 1)) {
            break; // 如果读取失败,说明连接已断开
        }

        // 5. 根据命令类型进行分发
        switch (cmd) {
            // ... 其他 case ...
            case RPC_CMD_GRAPH_COMPUTE: {
                // 接收计算图数据
                std::vector<uint8_t> input;
                if (!recv_msg(sockfd, input)) { return; }

                // 调用 rpc_server 的方法来处理
                rpc_msg_graph_compute_rsp response;
                if (!server.graph_compute(input, response)) { return; }

                // 发送响应
                if (!send_msg(sockfd, &response, sizeof(response))) { return; }
                break;
            }
            // ... 其他 case ...
        }
    }
}

最后,我们来看看 rpc_server::graph_compute 这个成员函数,它展示了如何将 RPC 请求与本地后端调用联系起来:

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

bool rpc_server::graph_compute(const std::vector<uint8_t> & input, ...) {
    // ... (反序列化 input 数据,在内存中重建出 ggml_cgraph* graph) ...
    // 这里省略了复杂的反序列化逻辑

    // 6. 调用本地后端执行计算!
    // this->backend 就是启动时传入的 CUDA 后端
    ggml_status status = ggml_backend_graph_compute(backend, graph);

    // 将结果存入 response
    response.result = status;
    return true;
}

这下,整个流程就完全打通了!客户端的请求通过网络,被服务器的 switch 语句捕获,最终触发了服务器本地 GPU 上的真正计算。

5. 总结与展望

在本章中,我们揭开了 ggml-rpc 系统另一半的神秘面纱------RPC 服务器 (rpc_server)

  • 我们理解了 rpc_server 是一个在远程机器上持续运行的服务程序 ,是所有计算任务的最终执行者
  • 我们学会了如何通过命令行启动 rpc_server,并指定它使用哪个本地后端(如 CUDA)来工作。
  • 我们深入探究了一个客户端请求在服务器端的完整处理流程:从接受连接,到解析命令,再到调用本地后端执行计算,最后返回结果。

至此,我们已经了解了客户端的"遥控器"和服务器端的"执行者"。你可能已经非常好奇了:它们之间通信时,发送的那些二进制数据到底是什么格式?计算图是如何被"打包"成字节流的?命令和响应的结构又是怎样的?

要回答这些问题,我们就必须深入了解它们之间共同遵守的"语言"。

相关推荐
智能汽车人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