大模型推理正在从"云端独占"走向"端侧可用"。过去我们做推理优化,更多关注的是服务器上的吞吐和并发;现在越来越多场景要求在一块消费级 GPU、一台边缘设备,甚至本地开发机上跑通 LLM。
本文拆解一个面向生产环境的轻量级 CUDA 大模型推理引擎,它主打 vLLM 风格的连续批处理(Continuous Batching) ,用 C++20 和 CUDA 手写核心推理路径,支持 Llama-3.2 和 Qwen3 系列模型。重点讲清楚它如何在 Paged Attention、INT8 量化、GPU 算子优化、两阶段调度器 这些方向上做工程化落地。
一、它解决什么问题?
这个推理引擎的核心定位不是"再训练一个大模型",而是把大模型推理链路做得更轻、更快、更适合生产部署。
它主要面向几类需求:
- 需要高并发推理服务,同时处理大量用户请求;
- 想理解 vLLM 的 Paged Attention 和连续批处理在 C++ 里怎么落地;
- 想研究 INT8 量化对推理速度和精度的影响;
- 想搭建一个兼容 OpenAI API 的本地推理服务;
- 想学习现代 C++20 在 AI 基础设施中的工程实践。
相比 PyTorch + Transformers 的完整管线,它更偏"精简实现 + 性能极致"。它没有追求覆盖所有模型家族,而是围绕 Llama 系列和 Qwen 系列,把推理里最关键的几个加速点打通。
性能数据
在 NVIDIA A100 上测试 Llama 3.2 1B 模型(INT8 量化)的表现:
| 场景 | 该引擎 | llama.cpp | 加速比 |
|---|---|---|---|
| 单请求推理 | 185 tok/s | 252 tok/s | 0.73x(单请求略慢) |
| 批处理 Batch=4 | 410 tok/s | 252 tok/s | 1.63x |
| 批处理 Batch=8 | 720 tok/s | 255 tok/s | 2.82x |
| 批处理 Batch=16 | 787 tok/s | 253 tok/s | 3.07x |
| TTFT(100 token prompt) | 387ms | - | - |
可以看到,单请求场景下它比 llama.cpp 慢一些------因为它的重心不在单请求延迟,而在批处理吞吐。当并发请求数上来后,连续批处理的优势就明显了:batch=16 时达到 787 tok/s,是 llama.cpp 的 3 倍多。
二、行业技术知识点
1. 连续批处理(Continuous Batching):vLLM 的核心思想
传统批处理有个痛点:一批请求里,有的请求生成得快,有的生成得慢。等慢的请求生成完了才能开始下一批,GPU 利用率很低。
连续批处理的核心思想是:在 token 级别动态调度请求,而不是等整批完成才切换。
具体做法:
- 调度器维护一个请求队列,每个请求有状态(等待中、运行中、已完成);
- 每一轮推理,先继续所有运行中的请求(不管它们还差多少 token 生成完);
- 然后从等待队列里塞入新请求,填满剩余 GPU 容量;
- 新请求可以随时加入,不用等当前批次结束。
这就好比高速公路上的车流------传统批处理是"等一整车人到了才发车",连续批处理是"随时有人上车,车满了就走"。
2. Paged Attention:块级 KV Cache 管理
LLM 推理中,KV Cache 是显存杀手。序列越长,占的显存越多。传统做法是预分配最大长度的显存,浪费严重。
Paged Attention 借鉴了操作系统虚拟内存的思想:
- 把 KV Cache 切成固定大小的块(Block),比如每个块 16 个 token;
- 每个请求动态分配需要的块,不用时释放;
- 用一个**块表(Block Table)**记录每个请求的块映射关系;
- Attention 计算时通过块表间接访问 KV Cache,而不是连续内存。
这样做的收益:
- 显存利用率大幅提升,不再预分配最大长度;
- 支持请求的动态创建和销毁;
- 块级操作可以批量执行,减少 kernel 启动开销。
3. INT8 量化:cuBLASLt 加速
INT8 量化把权重从 FP16(16 位浮点)压缩到 INT8(8 位整数),显存占用减半,理论计算吞吐量翻倍。
这个引擎的量化实现路径:
- 使用分组量化(Group-wise Quantization),每组 32/64/128 个权重共享一个 scale 参数;
- 利用 cuBLASLt 库的 INT8 GEMM 内核,调用 GPU 上的 Tensor Core 执行整数矩阵乘法;
- 量化后 perplexity 下降控制在 1% 以内,实际效果可接受。
4. GPU 算子优化手段
这个引擎在算子层面做了不少优化:
- float4 向量化加载:每次加载 4 个 float(16 字节),利用 GPU 内存带宽的 2-4 倍效率提升;
- 融合操作(Fused Operations):把采样、RoPE、归一化等操作融合到更少 kernel 里,减少全局内存访问;
- CUB BlockReduce 优化 Softmax:用 CUB 库的并行归约实现数值稳定的 softmax,避免溢出;
- 零拷贝设计:大量使用 std::span 和 move 语义,减少不必要的内存拷贝。
三、架构设计思路
整体架构可以分为四层:
+---------------------------------------------------------------+
| Web UI / HTTP API 层 |
| 内置 Web 界面 + OpenAI 兼容 API 服务 |
+---------------------------------------------------------------+
| C++ 推理引擎层(核心) |
| ContinuousBatchEngine + Scheduler + LLaMAModel |
+---------------------------------------------------------------+
| GPU 算子层 |
| MHA(RoPE) | RMSNorm | SwiGLU | Matmul | Embedding | Add |
+---------------------------------------------------------------+
| CUDA Kernel 层 |
| PagedAttention | float4加载 | CUB BlockReduce | GPU采样 |
+---------------------------------------------------------------+
项目目录结构:
project/
├── include/photon/
│ ├── arch/ # 模型架构定义(LLaMA、配置)
│ ├── core/ # 核心类型(Tensor、错误处理 Result<T,E>)
│ ├── io/ # 模型加载、分词器、checkpoint
│ ├── ops/ # 算子接口定义
│ │ └── kernels/ # CUDA kernel 实现
│ ├── runtime/ # 块管理器、KV缓存管理器
│ └── scheduler/ # 连续批处理调度器、引擎
├── src/
│ ├── arch/ # config.cpp, llama_model.cpp, transformer_block.cpp
│ ├── core/ # quant.cpp, tensor.cpp, thrust_allocator.cu
│ ├── io/ # checkpoint.cpp, model_loader.cpp, tokenizer.cpp
│ ├── ops/ # add.cpp, embedding.cpp, matmul.cpp,
│ │ # mha.cpp, rmsnorm.cpp, rope.cpp, swiglu.cpp
│ │ └── kernels/ # CUDA kernel 源码
│ └── runtime/ # block_manager.cpp, block_table.cpp, kv_cache_manager.cpp
├── web/ # Web UI 静态文件
├── docker/ # Docker 镜像配置
├── demo/ # 演示程序
├── tests/ # 单元测试
└── CMakeLists.txt # CMake 构建配置
这种分层的好处很明确:
- ops/ 层定义算子接口,arch/ 层组合算子构建模型,runtime/ 层管理内存,scheduler/ 层负责调度------职责清晰,各自独立演进;
- 通过 PHOTON_USE_CUDA 编译宏可以切换 CPU/CUDA 后端,方便调试;
- C++20 的 Concepts 和 Ranges 用在编译期约束算子接口,类型安全有保障。
四、核心代码实现原理
1. 连续批处理引擎:推理主循环
引擎的核心类是 ContinuousBatchEngine,它整合了调度器和模型,实现 token 级别的连续批处理。
构造函数接收模型引用、分词器、最大批次大小和 chunk size(预填充块大小,默认 256):
cpp
class ContinuousBatchEngine {
public:
ContinuousBatchEngine(
model::LLaMAModel& model,
const model::TikTokenizer& tokenizer,
i32 max_batch_size,
i32 chunk_size = 256);
i64 add_request(const std::string& prompt, i32 max_new_tokens);
Result<void> run_until_complete(bool print_progress = false);
std::string get_result(i64 request_id);
private:
Result<void> execute_batch_step(const SchedulerBatch& batch);
model::LLaMAModel& model_;
const model::TikTokenizer& tokenizer_;
i32 max_batch_size_;
i32 chunk_size_;
ContinuousBatchScheduler scheduler_;
#ifdef PHOTON_USE_CUDA
i32* sampled_tokens_gpu_;
#endif
};
run_until_complete 的主循环逻辑:
cpp
Result<void> run_until_complete(bool print_progress = false) {
i32 step = 0;
while (scheduler_.has_work()) {
// 1. 调度下一批
auto batch = scheduler_.schedule_next_batch();
if (batch.empty()) break;
// 2. 执行批次
auto result = execute_batch_step(batch);
if (!result) return Err<void>(result.error());
step++;
}
return Ok();
}
2. execute_batch_step:变长批处理的核心
这是整个引擎最核心的函数,实现了 vLLM 风格的变长批处理:
cpp
Result<void> execute_batch_step(const SchedulerBatch& batch) {
i32 num_seqs = batch.batch_size();
// 构建变长批次:把不同长度的请求flatten成统一表示
std::vector<i32> all_tokens;
std::vector<i32> all_positions;
std::vector<i32> seq_ids;
std::vector<i32> seq_lens;
for (i32 i = 0; i < num_seqs; ++i) {
auto& req = batch.requests[i];
// 获取下一个chunk(prefill请求处理chunk_size个token,decode请求处理1个)
auto chunk_tokens = req->get_next_chunk_tokens(chunk_size_);
auto chunk_positions = req->get_next_chunk_positions(chunk_size_);
i32 chunk_len = static_cast<i32>(chunk_tokens.size());
seq_lens.push_back(chunk_len);
// 追加到扁平化批次
all_tokens.insert(all_tokens.end(), chunk_tokens.begin(), chunk_tokens.end());
all_positions.insert(all_positions.end(), chunk_positions.begin(), chunk_positions.end());
// 重复seq_id用于chunk中的每个token
for (i32 j = 0; j < chunk_len; ++j) {
seq_ids.push_back(static_cast<i32>(req->request_id()));
}
}
i32 total_tokens = static_cast<i32>(all_tokens.size());
// 分配logits张量 [total_tokens, vocab_size]
auto logits_result = Tensor::create(
{total_tokens, model_.config().vocab_size},
DataType::Float32, DeviceType::CUDA);
// ...
// 前向传播(变长批次)
auto forward_result = model_.forward_batched(
all_tokens, all_positions, seq_ids, logits);
// ...
#ifdef PHOTON_USE_CUDA
cudaDeviceSynchronize();
// GPU采样 - 只对decode请求的最后一个token
std::vector<i32> sampled_tokens(num_seqs);
i32 token_offset = 0;
for (i32 i = 0; i < num_seqs; ++i) {
auto& req = batch.requests[i];
i32 chunk_len = seq_lens[i];
if (req->is_decode()) {
// 采样:从最后一个token的logits中取argmax
i32 logits_idx = token_offset + chunk_len - 1;
auto sampling_result = kernels::cuda::argmax_sampling_launch(
logits.ptr<f32>() + logits_idx * model_.config().vocab_size,
sampled_tokens_gpu_ + i,
1,
model_.config().vocab_size,
nullptr);
// ...
}
token_offset += chunk_len;
}
// 拷贝采样结果到CPU
cudaMemcpy(sampled_tokens.data(), sampled_tokens_gpu_,
num_seqs * sizeof(i32), cudaMemcpyDeviceToHost);
// 更新请求
std::vector<i64> finished_ids;
for (i32 i = 0; i < num_seqs; ++i) {
auto& req = batch.requests[i];
i32 chunk_len = seq_lens[i];
if (req->is_prefill()) {
// Prefill:只标记token为已计算
req->add_computed_tokens(chunk_len);
} else {
// Decode:添加生成的token
bool should_continue = req->add_token(
sampled_tokens[i], tokenizer_.eos_id());
if (!should_continue) {
finished_ids.push_back(req->request_id());
// 释放KV缓存
auto* cache_mgr = model_.paged_cache_manager();
cache_mgr->free_sequence(static_cast<i32>(req->request_id()));
}
}
}
// 更新调度器
scheduler_.update_after_step(finished_ids);
#endif
return Ok();
}
这段代码的关键设计点:
- 变长批次扁平化:不同请求可以有不同的 token 数量,通过 all_tokens/all_positions/seq_ids 三个数组把它们flatten成一个批次,seq_ids 标记每个 token 属于哪个请求;
- Prefill 和 Decode 分流:Prefill 请求一次性处理 chunk_size(默认 256)个 token,Decode 请求每次只处理 1 个 token;
- GPU 采样:只对 Decode 请求的最后一个 token 做 argmax 采样,Prefill 请求不采样(它们的 token 是已知的);
- KV Cache 管理:请求完成后立即释放 KV 缓存块,供新请求复用。
3. LLaMA 模型:前向传播流水线
LLaMAModel 类封装了完整的 LLaMA 架构前向传播:
cpp
class LLaMAModel {
public:
LLaMAModel(const TransformerConfig& config);
// 单token前向(用于prefill后的decode阶段)
Result<void> forward(i32 token, i32 pos, Tensor& logits);
// 批处理前向(用于连续批处理的变长批次)
Result<void> forward_batched(
const std::vector<i32>& tokens,
const std::vector<i32>& positions,
const std::vector<i32>& seq_ids,
Tensor& logits);
// 初始化Paged Attention缓存
Result<void> init_paged_cache(i32 num_blocks, i32 block_size);
// INT8量化
Result<void> quantize_weights(i32 group_size);
private:
TransformerConfig config_;
EmbeddingOp embedding_;
RMSNormOp final_norm_;
LinearOp classifier_;
std::vector<std::unique_ptr<TransformerBlock>> blocks_;
// KV Cache(非paged模式)
std::vector<Tensor> key_cache_;
std::vector<Tensor> value_cache_;
// Paged Attention
std::unique_ptr<KVCacheManager> paged_cache_manager_;
bool use_paged_cache_ = false;
// 批处理缓冲区
Tensor tokens_batch_;
Tensor x_batch_;
Tensor norm_batch_;
// CUDA资源
void* cublas_handle_ = nullptr;
i32* positions_gpu_ = nullptr;
i32* seq_ids_gpu_ = nullptr;
i32* cache_offsets_gpu_ = nullptr;
#ifdef PHOTON_USE_CUDA
i32 batched_buffers_capacity_ = 0;
#endif
};
单 token 前向传播流程(forward 函数):
cpp
Result<void> LLaMAModel::forward(i32 token, i32 pos, Tensor& logits) {
// 1. Embedding Lookup
auto token_tensor = Tensor::create({1}, DataType::Int32, config_.device);
// ... 拷贝token到GPU ...
auto emb_result = embedding_.forward(token_tensor.value(), emb_out_);
// ... 拷贝emb到x_ ...
// 2. 逐层前向传播
for (i32 layer_idx = 0; layer_idx < config_.n_layers; ++layer_idx) {
auto block_result = blocks_[layer_idx]->forward(
x_, pos, key_cache_[layer_idx], value_cache_[layer_idx]);
// ...
}
// 3. Final RMSNorm
auto norm_result = final_norm_.forward(x_, norm_out_);
// ...
// 4. Classifier Projection
// ...
return Ok();
}
批处理前向传播(forward_batched 函数)是性能关键路径:
cpp
Result<void> LLaMAModel::forward_batched(
const std::vector<i32>& tokens,
const std::vector<i32>& positions,
const std::vector<i32>& seq_ids,
Tensor& logits) {
// 确保预分配缓冲区足够
auto buffer_result = ensure_batched_buffers(total_tokens);
// ...
// 1. 拷贝输入到GPU(复用预分配缓冲区)
cudaMemcpy(tokens_batch_.data(), tokens.data(),
total_tokens * sizeof(i32), cudaMemcpyHostToDevice);
cudaMemcpy(positions_gpu_, positions.data(),
total_tokens * sizeof(i32), cudaMemcpyHostToDevice);
cudaMemcpy(seq_ids_gpu_, seq_ids.data(),
total_tokens * sizeof(i32), cudaMemcpyHostToDevice);
// 确保所有序列已分配KV缓存
if (use_paged_cache_) {
for (size_t i = 0; i < seq_ids.size(); ++i) {
i32 seq_id = seq_ids[i];
i32 pos = positions[i];
if (!paged_cache_manager_->is_sequence_allocated(seq_id)) {
auto alloc_result = paged_cache_manager_->allocate_sequence(
seq_id, pos + 256);
// ...
} else {
// 检查是否需要扩展
auto cap_result = paged_cache_manager_->get_sequence_capacity(seq_id);
if (cap_result && pos >= cap_result.value()) {
auto extend_result = paged_cache_manager_->extend_sequence(
seq_id, 128);
// ...
}
}
}
}
// 2. Batched Embedding Lookup
auto emb_result = embedding_.forward(tokens_batch_, x_batch_);
// ...
// 3. 逐层前向(BATCHED版本!使用Paged Attention)
if (use_paged_cache_) {
for (i32 layer_idx = 0; layer_idx < config_.n_layers; ++layer_idx) {
Tensor& key_cache = paged_cache_manager_->get_key_cache(layer_idx);
Tensor& value_cache = paged_cache_manager_->get_value_cache(layer_idx);
auto block_result = blocks_[layer_idx]->forward_batched(
x_batch_, positions_gpu_, positions, seq_ids, total_tokens,
key_cache, value_cache, paged_cache_manager_->get());
// ...
}
}
// 4. Batched Final RMSNorm
auto norm_result = final_norm_.forward(x_batch_, norm_batch_);
// ...
// 5. Batched Classifier
auto cls_result = classifier_.forward(norm_batch_, logits);
// ...
return Ok();
}
初始化 cuBLAS handle 用于 Tensor Core 优化:
cpp
Result<void> LLaMAModel::init() {
// 创建cuBLAS handle用于FP16 Tensor Core优化
if (config_.device == DeviceType::CUDA && cublas_handle_ == nullptr) {
cublasHandle_t handle;
cublasStatus_t status = cublasCreate(&handle);
// ...
cublas_set_math_mode(handle, CUBLAS_TENSOR_OP_MATH);
cublas_handle_ = static_cast<void*>(handle);
// 为所有transformer block设置cuBLAS handle
for (auto& block : blocks_) {
block->set_cublas_handle(cublas_handle_);
}
}
// ...
}
4. 多头注意力算子(MHA)
MHAOp 类实现了多头注意力的核心计算,支持 GQA(Grouped Query Attention):
cpp
class MHAOp : public OperatorBase<MHAOp> {
public:
explicit MHAOp(i32 dim, i32 kv_dim, i32 head_num, i32 head_size,
i32 seq_len, bool use_naive = false);
Result<void> forward(const Tensor& query, const Tensor& key_cache,
const Tensor& value_cache, Tensor& output, i32 pos);
// GQA支持:kv_mul = dim / kv_dim 控制KV head数与Q head数的比例
constexpr i32 kv_mul() const noexcept { return kv_mul_; }
private:
i32 dim_; // 总query维度 (head_num * head_size)
i32 kv_dim_; // 总key/value维度 (for GQA)
i32 head_num_; // Query head数
i32 head_size_; // 每个head的维度
i32 seq_len_; // 最大序列长度
i32 kv_mul_; // Q head数 / KV head数
bool use_naive_; // 是否使用朴素实现
};
内部计算流程:
- 对每个 head h 和位置 t < pos:scoreht = (Qh · Kht) / sqrt(head_size)
- 对 scores 应用 softmax:attenh = softmax(scoreh0:pos+1)
- 聚合 values:outputh = sum(attenht · Vht for t in 0:pos+1)
其中 kv_mul_ 参数控制 GQA:当 kv_mul_=1 时就是标准 Multi-Head Attention;当 kv_mul_>1 时多个 Q head 共享一个 KV head,减少 KV Cache 大小和 Attention 计算量。
5. KV Cache 管理器与块表
KVCacheManager 实现块级 KV 缓存分配:
cpp
class KVCacheManager {
public:
KVCacheManager(i32 num_blocks, i32 block_size, i32 n_layers,
i32 n_kv_heads, i32 head_size, DeviceType device);
Result<void> allocate_sequence(i32 seq_id, i32 required_tokens);
Result<void> extend_sequence(i32 seq_id, i32 additional_tokens);
void free_sequence(i32 seq_id);
bool is_sequence_allocated(i32 seq_id) const;
i32 get_sequence_capacity(i32 seq_id) const;
Tensor& get_key_cache(i32 layer_idx);
Tensor& get_value_cache(i32 layer_idx);
BlockTable& get_block_table();
private:
i32 num_blocks_;
i32 block_size_;
// ...
};
BlockTable 维护每个请求到物理块的映射:
cpp
class BlockTable {
public:
void add_block(i32 seq_id, i32 block_idx);
void free_blocks(i32 seq_id);
std::vector<i32> get_block_indices(i32 seq_id) const;
i32 allocate_block();
void free_block(i32 block_idx);
private:
std::unordered_map<i32, std::vector<i32>> block_tables_; // seq_id -> blocks
std::vector<bool> free_blocks_;
i32 next_free_block_ = 0;
// ...
};
6. 连续批处理调度器
ContinuousBatchScheduler 实现两阶段调度:
cpp
class ContinuousBatchScheduler {
public:
ContinuousBatchScheduler(i32 max_batch_size, i32 max_seqs,
i32 chunk_size,
SchedulerPolicy policy);
i64 add_request(const std::string& prompt,
const std::vector<i32>& tokens,
i32 max_new_tokens);
SchedulerBatch schedule_next_batch();
void update_after_step(const std::vector<i64>& finished_ids);
bool has_work() const;
SchedulerStats get_stats() const;
private:
// 请求状态机
enum class RequestState { WAITING, RUNNING, FINISHED, PREEMPTED };
// 两阶段调度:
// Phase 1: 继续所有RUNNING请求
// Phase 2: 接纳WAITING请求填充剩余容量
// ...
};
调度器维护的请求状态转换:
WAITING ──→ RUNNING ──→ FINISHED
│ │
│ └──→ PREEMPTED ──→ WAITING (预empt后重新进入等待队列)
└──────────────────────────→ (新请求直接进入WAITING)
Token 级别的 num_computed_tokens 追踪确保:
- Prefill 阶段:每次处理 chunk_size 个 token,num_computed_tokens 增加 chunk_size;
- Decode 阶段:每次生成 1 个 token,num_computed_tokens 增加 1;
- 当 num_computed_tokens >= max_new_tokens 时,请求标记为 FINISHED;
- 调度器根据 num_computed_tokens 精确决定每个请求下一个 step 需要处理多少 token。
7. INT8 量化实现
量化流程在模型层统一调用:
cpp
Result<void> LLaMAModel::quantize_weights(i32 group_size) {
std::cout << "Quantizing model weights to INT8 (group_size="
<< group_size << ")..." << std::endl;
// 量化所有transformer blocks
for (size_t i = 0; i < blocks_.size(); ++i) {
std::cout << " Layer " << i << "/" << blocks_.size() << "..." << std::flush;
auto result = blocks_[i]->quantize_weights(group_size);
if (!result) {
return Err<void>(result.error().code(),
"Failed to quantize layer " + std::to_string(i) + ": " +
result.error().message());
}
std::cout << " done" << std::endl;
}
// 量化classifier
std::cout << " Classifier..." << std::flush;
auto classifier_result = classifier_.quantize_weights(group_size);
// ...
std::cout << "Done! Model memory reduced by ~3.8x" << std::endl;
return Ok();
}
量化后模型内存减少约 3.8 倍(因为除了 embedding 和 classifier 层保持 FP16,中间层全部量化为 INT8)。
五、环境配置与运行测试教程
If you need the complete source code, please add the WeChat number (c17865354792)
1. 基础依赖
| 依赖 | 版本要求 | 说明 |
|---|---|---|
| 编译器 | GCC 12+ | 需要完整的 C++20 支持 |
| CMake | 3.20+ | 构建系统 |
| CUDA Toolkit | 12.0+ | 已在 12.5 上测试通过 |
| GPU | Compute Capability 7.0+ | Volta/Turing/Ampere/Hopper 架构 |
2. 从源码编译
bash
# 配置构建(开启CUDA)
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DPHOTON_BUILD_CUDA=ON ..
# 编译
cmake --build . -j$(nproc)
# 安装(可选)
sudo cmake --install .
安装后文件分布:
| 文件 | 安装路径 |
|---|---|
| photon_web_server | /usr/local/bin/ |
| 静态Web文件 | /该项目/web/static/ |
| 核心库 | /usr/local/lib/ |
卸载:
bash
cd build
sudo cmake --build . --target uninstall
3. 下载模型
需要从 HuggingFace 下载量化后的模型权重。以 Llama 3.2 1B 为例:
bash
# 安装huggingface-cli
pip install huggingface-cli
# 下载模型
huggingface-cli download Lummy666/llama-3.2-1B-Instruct \
--local-dir ./models/llama-3.2-1B-Instruct
4. 运行 Web 服务器
bash
photon_web_server \
--port 5728 \
--model /path/to/llama-3.2-1B-Instruct \
--tokenizer /path/to/llama-3.2-1B-Instruct/tokenizer.json
启动后访问 本地Web界面 即可使用内置 Web UI 进行对话。
5. Docker 方式运行(推荐)
如果不想手动配置环境,可以使用预构建镜像:
bash
# 拉取镜像
docker pull 镜像仓库/该项目:latest
# 运行容器(需要GPU支持)
docker run --rm --gpus all -p 5728:5728 -e PORT=5728 镜像仓库/该项目:latest
浏览器访问 本地Web界面 即可使用。
6. 性能测试
可以通过调整 batch size 测试批处理吞吐:
bash
# 使用Web UI同时发起多个请求,观察吞吐变化
# 或使用内置benchmark工具(如果有)
预期结果(A100 + Llama 3.2 1B + INT8):
| Batch Size | 预期吞吐 | 对比 llama.cpp |
|---|---|---|
| 1 | 185 tok/s | 0.73x |
| 4 | 410 tok/s | 1.63x |
| 8 | 720 tok/s | 2.82x |
| 16 | 787 tok/s | 3.07x |
7. 常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 编译报错 C++20 不支持 | GCC 版本过低 | 升级 GCC 到 12+ |
| CUDA kernel 编译失败 | CUDA Toolkit 版本不匹配 | 检查 nvcc --version,确保 12.0+ |
| 显存溢出 | 模型太大或 batch 太大 | 减小 max_batch_size 或使用量化模型 |
| 推理结果异常 | 模型文件不完整 | 重新下载模型,检查文件完整性 |
| Web UI 无法访问 | 端口被占用 | 使用 --port 参数指定其他端口 |
六、落地用途
1. 高并发推理服务
连续批处理的核心优势就是高并发场景。如果你需要一个同时服务几十个甚至上百个用户的 LLM API 服务,这个引擎的调度器设计就是为了这个场景优化的。batch=16 时 787 tok/s 的吞吐意味着单卡可以服务大量并发请求。
2. 生产环境部署
C++ 编译出的二进制文件没有 Python 运行时依赖,部署简单。配合 Docker 可以一键部署到任何支持 GPU 的服务器。INT8 量化进一步降低了显存需求,让中等规格 GPU 也能跑大模型。
3. 算子研究与优化实验
引擎的算子层(ops/)设计清晰,每个算子(MHA、RMSNorm、SwiGLU、Matmul 等)都是独立的类,方便替换和实验。比如你想试一个新的 Attention 实现,只需要改 ops/ 下的代码,不影响其他层。
4. 教学与学习
代码全部开源,C++20 风格清晰,是学习 LLM 推理引擎内部实现的好材料。从 Tensor 类到 Attention 算子到调度器,整个管线一目了然。特别是连续批处理和 Paged Attention 的实现,比看论文伪代码直观得多。
5. 边缘设备原型验证
虽然目前主要面向 A100 等高端 GPU,但 INT8 量化的支持意味着在算力有限的设备上也有用武之地。可以先在这个引擎上做算法验证,再移植到更受限的嵌入式平台。
七、总结
这个推理引擎的价值不在于堆模型数量,而在于把生产级 LLM 推理的关键技术串成了一条完整的管线:
- 算子层:用 C++20 实现高效的 MHA、RMSNorm、SwiGLU 等算子,配合 float4 向量化加载和 CUB 归约优化;
- 模型层:完整实现 LLaMA 架构,支持 FP32/INT8 精度;
- 内存层:Paged Attention 块级 KV Cache 管理,显存利用率最大化;
- 调度层:vLLM 风格的连续批处理,token 级动态调度,生产级并发能力;
- 工程层:CMake 构建、Docker 部署、Web UI + OpenAI API 双接口。
未来路线图包括 Flash Attention 2(IO 感知的长序列注意力)、多 GPU 张量并行、FP16/BF16 混合精度、以及投机解码(Speculative Decoding)等方向。
如果你正在做 LLM 推理相关的研究或产品开发,建议亲自跑通一次编译、推理、量化的完整流程。很多细节只有看到实际的 tok/s 数字和显存占用时,才会真正变得清晰。
Welcome to follow WeChat official account【程序猿编码】