vLLM部署GLM-4-9B-Chat-1M:C++高性能接口开发
vLLM部署GLM-4-9B-Chat-1M:C++高性能接口开发
1. 为什么需要C++接口调用vLLM服务
在实际工程落地中,很多业务系统是基于C++构建的,比如高频交易系统、实时推荐引擎、工业控制平台等。这些系统对延迟极其敏感,要求毫秒级响应,同时需要与现有C++生态无缝集成。虽然Python是AI开发的主流语言,但直接在生产环境中用Python调用vLLM服务会面临几个现实问题:Python解释器的GIL限制了多线程性能,序列化开销增加了请求延迟,内存管理不够精细导致资源浪费,还有与现有C++代码库的集成成本。
我最近在一个智能客服后台系统中就遇到了类似情况。系统原本用C++处理用户请求路由和会话管理,当需要接入大模型能力时,如果采用Python微服务方式,每次请求都要经过HTTP序列化、网络传输、反序列化,端到端延迟从20ms增加到了150ms以上,完全无法满足SLA要求。后来我们改用C++直接对接vLLM的gRPC接口,不仅把延迟压回到了35ms以内,还省去了维护Python服务集群的运维成本。
vLLM本身提供了完善的C++ SDK支持,通过gRPC协议暴露服务,这让我们能绕过Python层,直接在C++代码中发起推理请求。这种方式特别适合那些对性能有极致要求,又不想重构整个技术栈的场景。接下来我会带你一步步实现这个过程,从环境准备到内存优化,全部用可运行的代码说话。
2. 环境搭建与vLLM服务启动
2.1 准备硬件与基础环境
GLM-4-9B-Chat-1M模型支持100万token上下文长度,这对硬件提出了较高要求。根据官方测试数据,要流畅运行1M上下文,建议配置至少4张80GB显存的A100或H100 GPU。不过对于大多数实际应用场景,我们通常不需要满负荷使用1M上下文,可以适当降低配置要求。
我推荐的入门配置是:
- GPU:2张NVIDIA A10(24GB显存)或1张A100(40GB)
- CPU:16核以上
- 内存:64GB以上
- 存储:SSD,至少200GB可用空间
安装基础依赖:
# 更新系统包
sudo apt update && sudo apt upgrade -y
# 安装CUDA 12.1(vLLM 0.4.x版本推荐)
wget https://developer.download.nvidia.com/compute/cuda/12.1.1/local_installers/cuda_12.1.1_530.30.02_linux.run
sudo sh cuda_12.1.1_530.30.02_linux.run --silent --override
# 安装Python 3.10
sudo apt install python3.10 python3.10-venv python3.10-dev -y
# 创建虚拟环境
python3.10 -m venv vllm-env
source vllm-env/bin/activate
2.2 部署vLLM服务
vLLM提供了两种部署方式:Python命令行启动和Docker容器启动。考虑到生产环境的稳定性,我更推荐Docker方式,特别是使用国内镜像源避免网络问题。
首先拉取国内优化的vLLM镜像:
# 使用阿里云镜像(国内访问更快)
docker pull egs-registry.cn-hangzhou.cr.aliyuncs.com/egs/vllm:0.4.0.post1-pytorch2.1.2-cuda12.1.1-cudnn8-ubuntu22.04
然后下载GLM-4-9B-Chat-1M模型。由于模型较大(约18GB),建议使用ModelScope下载:
pip install modelscope
modelscope download --model ZhipuAI/glm-4-9b-chat-1m --cache-dir /data/models
启动vLLM服务容器,关键是要启用gRPC接口并正确配置参数:
docker run -d \
--name vllm-glm4 \
--gpus all \
--ipc=host \
--ulimit memlock=-1 \
--ulimit stack=67108864 \
-v /data/models:/models \
-p 8000:8000 \
-p 50051:50051 \
egs-registry.cn-hangzhou.cr.aliyuncs.com/egs/vllm:0.4.0.post1-pytorch2.1.2-cuda12.1.1-cudnn8-ubuntu22.04 \
python -m vllm.entrypoints.openai.api_server \
--host 0.0.0.0 \
--port 8000 \
--grpc-port 50051 \
--model /models/ZhipuAI/glm-4-9b-chat-1m \
--tensor-parallel-size 2 \
--max-model-len 131072 \
--dtype bfloat16 \
--trust-remote-code \
--enforce-eager \
--enable-prefix-caching \
--disable-log-requests
这里有几个关键参数需要特别注意:
--grpc-port 50051:启用gRPC服务端口,这是C++客户端连接的关键--max-model-len 131072:设置为128K而非1M,平衡性能和资源消耗--tensor-parallel-size 2:双GPU并行,充分利用硬件资源--enforce-eager:禁用CUDA图优化,提高首次推理速度
启动后,可以用curl测试HTTP接口是否正常:
curl http://localhost:8000/v1/models
如果返回包含glm-4-9b-chat-1m的JSON数据,说明服务已成功启动。
3. C++客户端开发实战
3.1 gRPC接口定义与代码生成
vLLM的gRPC接口定义在vllm/proto/vllm.proto文件中。我们需要先生成C++代码。如果你没有安装protobuf编译器,先安装:
# 安装protobuf编译器
sudo apt install protobuf-compiler libprotobuf-dev -y
# 生成C++代码(假设你已经克隆了vLLM源码)
cd vllm
protoc --cpp_out=. proto/vllm.proto
生成的头文件vllm.pb.h和源文件vllm.pb.cc就是我们的C++客户端基础。为了简化开发,我封装了一个轻量级的C++客户端类,核心功能包括连接管理、请求构造和响应解析。
3.2 核心C++客户端实现
下面是一个完整的C++客户端实现,包含了连接池、请求重试和内存管理:
// vllm_client.h
#pragma once
#include <grpcpp/grpcpp.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include <memory>
#include <string>
#include <vector>
#include <mutex>
#include <queue>
#include <thread>
#include <chrono>
// 自动生成的proto头文件
#include "vllm.pb.h"
class VLLMClient {
public:
explicit VLLMClient(const std::string& server_address);
// 同步推理请求
bool generate(const std::string& prompt,
std::string& response,
int max_tokens = 1024,
float temperature = 0.7f,
int top_k = 50);
// 异步推理请求(推荐用于高并发场景)
struct AsyncRequest {
std::string prompt;
int max_tokens;
float temperature;
int top_k;
std::function<void(bool, const std::string&)> callback;
};
void async_generate(const AsyncRequest& req);
// 连接状态检查
bool is_connected() const;
private:
std::shared_ptr<grpc::Channel> channel_;
std::unique_ptr<vllm::VLLM::Stub> stub_;
mutable std::mutex mutex_;
std::atomic<bool> connected_{false};
// 连接重试机制
bool connect_with_retry(int max_retries = 3);
// 请求重试逻辑
bool send_request_with_retry(const vllm::GenerateRequest& request,
vllm::GenerateResponse* response,
grpc::ClientContext* context);
};
// vllm_client.cpp
#include "vllm_client.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <condition_variable>
VLLMClient::VLLMClient(const std::string& server_address)
: channel_(grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials())),
stub_(vllm::VLLM::NewStub(channel_)) {
if (!connect_with_retry()) {
std::cerr << "Failed to connect to vLLM server at " << server_address << std::endl;
}
}
bool VLLMClient::connect_with_retry(int max_retries) {
for (int i = 0; i < max_retries; ++i) {
grpc::ChannelArguments args;
args.SetMaxSendMessageSize(100 * 1024 * 1024); // 100MB
args.SetMaxReceiveMessageSize(100 * 1024 * 1024);
auto channel = grpc::CreateCustomChannel(
"localhost:50051",
grpc::InsecureChannelCredentials(),
args
);
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(5));
vllm::HealthCheckRequest health_req;
vllm::HealthCheckResponse health_resp;
auto status = stub_->HealthCheck(&context, health_req, &health_resp);
if (status.ok() && health_resp.status() == vllm::HealthCheckResponse::SERVING) {
channel_ = channel;
stub_ = vllm::VLLM::NewStub(channel_);
connected_ = true;
return true;
}
if (i < max_retries - 1) {
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
return false;
}
bool VLLMClient::generate(const std::string& prompt,
std::string& response,
int max_tokens,
float temperature,
int top_k) {
if (!is_connected()) {
return false;
}
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(60));
vllm::GenerateRequest request;
request.set_prompt(prompt);
request.set_max_tokens(max_tokens);
request.set_temperature(temperature);
request.set_top_k(top_k);
vllm::GenerateResponse response_proto;
auto status = send_request_with_retry(request, &response_proto, &context);
if (status) {
response = response_proto.text();
return true;
}
return false;
}
bool VLLMClient::send_request_with_retry(const vllm::GenerateRequest& request,
vllm::GenerateResponse* response,
grpc::ClientContext* context) {
for (int i = 0; i < 3; ++i) {
try {
auto status = stub_->Generate(context, request, response);
if (status.ok()) {
return true;
}
if (i < 2) {
std::this_thread::sleep_for(std::chrono::milliseconds(100 * (i + 1)));
}
} catch (const std::exception& e) {
if (i < 2) {
std::this_thread::sleep_for(std::chrono::milliseconds(100 * (i + 1)));
}
}
}
return false;
}
bool VLLMClient::is_connected() const {
return connected_.load();
}
3.3 高性能调用示例
现在我们来写一个实际的调用示例,展示如何在C++中高效使用这个客户端:
// main.cpp
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <chrono>
#include <memory>
#include "vllm_client.h"
// 模拟一个简单的聊天应用
class ChatApplication {
private:
std::unique_ptr<VLLMClient> client_;
std::string system_prompt_;
public:
ChatApplication() : client_(std::make_unique<VLLMClient>("localhost:50051")) {
system_prompt_ = "你是一个专业的智能客服助手,回答要简洁准确,不超过100字。";
}
// 单次对话
std::string chat(const std::string& user_input) {
std::string full_prompt = system_prompt_ + "\n用户:" + user_input + "\n助手:";
std::string response;
auto start_time = std::chrono::high_resolution_clock::now();
if (client_->generate(full_prompt, response, 512, 0.3f, 10)) {
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
end_time - start_time).count();
std::cout << "[INFO] 推理耗时: " << duration << "ms" << std::endl;
return response;
}
return "抱歉,服务暂时不可用";
}
// 批量处理(模拟高并发场景)
void batch_process(const std::vector<std::string>& inputs) {
std::vector<std::thread> threads;
for (const auto& input : inputs) {
threads.emplace_back([this, input]() {
std::string result = chat(input);
std::cout << "[BATCH] 输入: " << input.substr(0, 30)
<< "... -> 输出: " << result.substr(0, 50) << "..." << std::endl;
});
}
for (auto& t : threads) {
t.join();
}
}
};
int main() {
ChatApplication app;
// 测试单次调用
std::cout << "=== 单次调用测试 ===" << std::endl;
std::string response1 = app.chat("今天天气怎么样?");
std::cout << "Q: 今天天气怎么样?" << std::endl;
std::cout << "A: " << response1 << std::endl << std::endl;
// 测试批量处理
std::cout << "=== 批量处理测试 ===" << std::endl;
std::vector<std::string> test_inputs = {
"帮我写一封辞职信",
"Python中如何处理异常?",
"推荐几本学习机器学习的好书",
"上海到北京的高铁时刻表"
};
auto batch_start = std::chrono::high_resolution_clock::now();
app.batch_process(test_inputs);
auto batch_end = std::chrono::high_resolution_clock::now();
auto batch_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
batch_end - batch_start).count();
std::cout << "[BATCH TOTAL] 批量处理总耗时: " << batch_duration << "ms" << std::endl;
return 0;
}
3.4 编译与运行
创建CMakeLists.txt文件:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(vllm_cpp_client)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Release)
# 查找gRPC和protobuf
find_package(gRPC CONFIG REQUIRED)
find_package(Protobuf REQUIRED)
# 生成的proto文件
set(PROTO_SRCS vllm.pb.cc)
set(PROTO_HDRS vllm.pb.h)
# 可执行文件
add_executable(vllm_client main.cpp vllm_client.cpp ${PROTO_SRCS})
target_include_directories(vllm_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(vllm_client PRIVATE ${Protobuf_INCLUDE_DIRS})
# 链接库
target_link_libraries(vllm_client PRIVATE
${Protobuf_LIBRARIES}
${gRPC_LIBRARIES}
gpr
grpc++
grpc
pthread
)
# 设置编译选项
target_compile_options(vllm_client PRIVATE -O3 -Wall -Wextra)
编译运行:
mkdir build && cd build
cmake ..
make -j$(nproc)
./vllm_client
4. 内存管理与性能优化技巧
4.1 内存池优化策略
在高并发场景下,频繁的内存分配释放会成为性能瓶颈。我们可以通过自定义内存池来优化:
// memory_pool.h
#pragma once
#include <memory>
#include <vector>
#include <mutex>
#include <queue>
template<typename T>
class MemoryPool {
private:
struct Block {
T* data;
size_t capacity;
std::queue<T*> free_list;
Block(size_t cap) : capacity(cap) {
data = new T[cap];
for (size_t i = 0; i < cap; ++i) {
free_list.push(&data[i]);
}
}
~Block() {
delete[] data;
}
};
std::vector<std::unique_ptr<Block>> blocks_;
std::mutex mutex_;
size_t block_size_;
public:
explicit MemoryPool(size_t block_size = 1024) : block_size_(block_size) {}
T* allocate() {
std::lock_guard<std::mutex> lock(mutex_);
// 尝试从现有块中分配
for (auto& block : blocks_) {
if (!block->free_list.empty()) {
T* ptr = block->free_list.front();
block->free_list.pop();
return ptr;
}
}
// 创建新块
auto new_block = std::make_unique<Block>(block_size_);
blocks_.push_back(std::move(new_block));
return blocks_.back()->free_list.front();
}
void deallocate(T* ptr) {
std::lock_guard<std::mutex> lock(mutex_);
// 实际项目中需要找到对应的块,这里简化处理
}
};
// 在客户端中使用内存池管理请求对象
class OptimizedVLLMClient : public VLLMClient {
private:
MemoryPool<vllm::GenerateRequest> request_pool_;
MemoryPool<vllm::GenerateResponse> response_pool_;
public:
OptimizedVLLMClient(const std::string& server_address)
: VLLMClient(server_address),
request_pool_(100),
response_pool_(100) {}
bool optimized_generate(const std::string& prompt, std::string& response) {
auto* req = request_pool_.allocate();
auto* resp = response_pool_.allocate();
req->set_prompt(prompt);
req->set_max_tokens(512);
req->set_temperature(0.3f);
grpc::ClientContext context;
auto status = stub_->Generate(&context, *req, resp);
if (status.ok()) {
response = resp->text();
}
// 归还内存
request_pool_.deallocate(req);
response_pool_.deallocate(resp);
return status.ok();
}
};
4.2 连接复用与请求批处理
gRPC连接建立有开销,我们应该复用连接而不是每次创建新连接。同时,vLLM支持批量请求,可以显著提升吞吐量:
// batch_client.h
#pragma once
#include <grpcpp/grpcpp.h>
#include <vector>
#include <string>
class BatchVLLMClient {
private:
std::shared_ptr<grpc::Channel> channel_;
std::unique_ptr<vllm::VLLM::Stub> stub_;
public:
explicit BatchVLLMClient(const std::string& server_address);
// 批量推理
bool batch_generate(const std::vector<std::string>& prompts,
std::vector<std::string>& responses,
int max_tokens = 512);
private:
// 内部批处理函数
bool process_batch(const std::vector<std::string>& prompts,
std::vector<std::string>& responses,
int max_tokens);
};
// batch_client.cpp
#include "batch_client.h"
#include <iostream>
BatchVLLMClient::BatchVLLMClient(const std::string& server_address)
: channel_(grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials())),
stub_(vllm::VLLM::NewStub(channel_)) {}
bool BatchVLLMClient::batch_generate(const std::vector<std::string>& prompts,
std::vector<std::string>& responses,
int max_tokens) {
if (prompts.empty()) return false;
responses.clear();
responses.resize(prompts.size());
// 分批次处理,避免单次请求过大
const size_t batch_size = 8;
for (size_t i = 0; i < prompts.size(); i += batch_size) {
size_t end = std::min(i + batch_size, prompts.size());
std::vector<std::string> batch(prompts.begin() + i, prompts.begin() + end);
if (!process_batch(batch, responses, max_tokens)) {
return false;
}
}
return true;
}
bool BatchVLLMClient::process_batch(const std::vector<std::string>& prompts,
std::vector<std::string>& responses,
int max_tokens) {
grpc::ClientContext context;
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(30));
vllm::BatchGenerateRequest request;
for (const auto& prompt : prompts) {
request.add_prompts(prompt);
}
request.set_max_tokens(max_tokens);
vllm::BatchGenerateResponse response;
auto status = stub_->BatchGenerate(&context, request, &response);
if (status.ok()) {
for (int i = 0; i < response.responses_size(); ++i) {
responses[i] = response.responses(i).text();
}
return true;
}
return false;
}
4.3 性能调优实践建议
在实际项目中,我总结了几个关键的性能调优点:
1. 显存优化
- 使用
--kv-cache-dtype fp8参数启用FP8 KV缓存,可减少约40%显存占用 - 对于长文本场景,启用
--enable-chunked-prefill,虽然会略微增加prefill时间,但能显著降低峰值显存 - 设置合理的
--max-num-batched-tokens(建议8192-16384),避免单次请求过大
2. CPU-GPU协同
- 在vLLM启动时添加
--worker-cls vllm.engine.multiproc_gpu_executor.MultiprocessingGPUExecutor,启用多进程GPU执行器 - 使用
--num-scheduler-steps 2增加调度器步数,提高GPU利用率
3. 网络优化
- 在C++客户端中设置合适的gRPC参数:
grpc::ChannelArguments args;
args.SetMaxSendMessageSize(50 * 1024 * 1024); // 50MB
args.SetMaxReceiveMessageSize(50 * 1024 * 1024);
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 30000);
args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 10000);
args.SetInt(GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA, 0);
args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1);
4. 应用层优化
- 实现请求队列和优先级调度,避免突发流量打垮服务
- 添加响应缓存,对相同prompt的请求直接返回缓存结果
- 使用异步IO处理大量并发请求,避免线程阻塞
5. 常见问题与解决方案
5.1 连接超时与重连
在生产环境中,网络波动可能导致连接中断。我们的客户端实现了自动重连机制,但还需要处理一些边界情况:
// 增强版重连逻辑
bool VLLMClient::reconnect_if_needed() {
if (is_connected()) return true;
std::lock_guard<std::mutex> lock(mutex_);
if (is_connected()) return true;
// 指数退避重连
static std::chrono::milliseconds backoff(100);
static std::mutex backoff_mutex;
{
std::lock_guard<std::mutex> lock(backoff_mutex);
if (backoff.count() < 5000) { // 最大5秒
backoff = std::chrono::milliseconds(backoff.count() * 2);
}
}
std::this_thread::sleep_for(backoff);
return connect_with_retry(1);
}
5.2 中文乱码与编码问题
GLM-4系列模型对中文支持很好,但在C++中处理UTF-8字符串时需要注意:
// 字符串工具函数
namespace StringUtils {
// 检查UTF-8字符串是否有效
bool is_valid_utf8(const std::string& str) {
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(str.c_str());
size_t len = str.length();
for (size_t i = 0; i < len; ++i) {
if ((bytes[i] & 0x80) == 0) continue; // ASCII
int bytes_needed = 0;
if ((bytes[i] & 0xE0) == 0xC0) bytes_needed = 2;
else if ((bytes[i] & 0xF0) == 0xE0) bytes_needed = 3;
else if ((bytes[i] & 0xF8) == 0xF0) bytes_needed = 4;
else return false;
if (i + bytes_needed > len) return false;
for (int j = 1; j < bytes_needed; ++j) {
if ((bytes[i + j] & 0xC0) != 0x80) return false;
}
i += bytes_needed - 1;
}
return true;
}
// 截断UTF-8字符串到指定字符数(不是字节数)
std::string truncate_utf8(const std::string& str, size_t max_chars) {
if (max_chars == 0) return "";
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(str.c_str());
size_t len = str.length();
size_t chars_count = 0;
size_t pos = 0;
while (pos < len && chars_count < max_chars) {
if ((bytes[pos] & 0x80) == 0) {
// ASCII字符
++pos;
} else if ((bytes[pos] & 0xE0) == 0xC0) {
// 2字节UTF-8
pos += 2;
} else if ((bytes[pos] & 0xF0) == 0xE0) {
// 3字节UTF-8
pos += 3;
} else if ((bytes[pos] & 0xF8) == 0xF0) {
// 4字节UTF-8
pos += 4;
} else {
break; // 无效UTF-8
}
++chars_count;
}
return str.substr(0, pos);
}
}
5.3 错误处理与日志
生产环境需要完善的错误处理和日志记录:
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
class RobustVLLMClient : public VLLMClient {
private:
std::shared_ptr<spdlog::logger> logger_;
public:
RobustVLLMClient(const std::string& server_address)
: VLLMClient(server_address) {
logger_ = spdlog::stdout_color_mt("vllm_client");
logger_->set_level(spdlog::level::info);
}
bool generate_with_logging(const std::string& prompt,
std::string& response,
int max_tokens = 1024) {
auto start = std::chrono::steady_clock::now();
logger_->info("Starting inference for prompt: {}",
StringUtils::truncate_utf8(prompt, 50));
bool success = generate(prompt, response, max_tokens);
auto end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
if (success) {
logger_->info("Inference completed successfully in {}ms, response length: {} chars",
duration, response.length());
} else {
logger_->error("Inference failed after {}ms", duration);
}
return success;
}
};
6. 总结
用C++开发vLLM高性能接口,本质上是在AI能力和系统性能之间找到最佳平衡点。从我的实践经验来看,关键不在于追求理论上的极致性能,而在于理解业务场景的真实需求。
比如在智能客服场景中,我们发现95%的请求都是短文本问答,真正需要长上下文的不到5%。因此我们采用了混合策略:对普通请求使用128K上下文配置,对特殊需求才动态切换到更高配置。这样既保证了大部分请求的低延迟,又避免了资源浪费。
另一个重要体会是,C++的优势不仅在于执行速度快,更在于它能让我们对整个技术栈有完全的掌控力。我们可以精确控制内存分配、网络连接、线程调度等每一个环节,这种可控性在生产环境中价值巨大。
最后想说的是,技术选型没有银弹。C++方案适合那些对性能有严苛要求、已有成熟C++技术栈的团队。如果你的团队主要使用Java或Go,那么相应的客户端方案可能更适合。关键是根据团队能力和业务需求做出务实的选择。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐

所有评论(0)