必须用线程池而非std::thread:因线程创建销毁开销大、调度压力高,生产中需预启固定线程、无锁队列、cpu绑定、防伪共享,并在io处挂起协程,动态批处理须分桶与重排kv缓存。

std::thread 与线程池选哪个?别直接 new thread
直接用 std::thread 每个请求起一个线程,在 LLM 推理场景下等于主动制造性能雪崩。线程创建/销毁开销大,OS 调度压力陡增,且容易触发 std::system_error(资源不足)。真实生产中必须用线程池——但不是简单封装 std::queue + std::thread,得带任务窃取或负载感知。
推荐做法:
- 用
std::vector<:thread></:thread>预启动固定数量工作线程(通常设为物理核心数 × 1~2),避免运行时扩容 - 任务队列必须是无锁的,比如
moodycamel::ConcurrentQueue或自研环形缓冲区,否则std::mutex会成为吞吐瓶颈 - 每个线程绑定到特定 CPU 核心(
pthread_setaffinity_np或numa_bind),防止缓存行迁移 - 避免在线程池里做模型加载、分词等重 IO 操作——这些应前置到主线程或专用 IO 线程
模型参数共享时怎么避免 false sharing 和 cache line bouncing
多个线程并发读权重矩阵(尤其是 attention 的 q_proj、k_proj)时,若不同线程访问相邻内存地址,哪怕只是只读,也可能因缓存一致性协议反复同步同一 cache line,造成显著延迟。
关键对策:
- 对线程局部状态结构体强制
alignas(64),确保每个实例独占 cache line - 权重矩阵按 block 切分后,让每个线程处理不重叠的 column range,而非 row range(减少跨 cache line 访问)
- 禁用编译器自动向量化(
-fno-tree-vectorize)对小尺寸 tensor,避免生成非对齐访存指令 - GPU 场景下,显存访问模式比 CPU 更敏感——用
__ldg(CUDA)或rocdl.lds(HIP)显式提示只读缓存
gRPC + C++20 协程如何真正降低推理延迟
单纯把 std::async 换成协程,不改调度逻辑,延迟几乎没变化。真正起效的是:协程挂起点必须落在 I/O 等待处(如 KV cache 读写、网络收发),而不是计算密集段。
典型误用:
- 在
forward()内部直接co_await—— 这会让 GPU kernel 启动变成异步,反而增加调度开销 - 协程 resume 在随机线程上执行,导致 NUMA 跨节点内存访问
正确姿势:
- 用
std::coroutine_handle封装任务,挂起点仅设在recv_from_client和send_to_client两处 - resume 时显式调度到原线程(通过
thread_local记录归属线程 ID) - 配合
std::shared_mutex保护模型状态,但只在模型热更新(如版本切换)时写锁,推理全程只读
动态批处理(Dynamic Batching)的 C++ 实现陷阱
很多团队照搬 Triton 的 batch window 逻辑,但在 C++ 自研系统里常忽略两个硬约束:内存连续性 & 时间精度。
问题表现:
- batch 内 token 数差异过大(如 [128, 512, 32]),导致 padding 后显存占用爆炸
- 用
std::chrono::steady_clock::now()做超时判断,但未考虑线程调度延迟,实际窗口漂移达毫秒级 - batch 合并后未重排 KV cache 的 layout,导致后续 decode 步骤 cache miss 率上升 40%+
实操建议:
- 按 token 数分桶(如 64/128/256/512),同桶内请求才合并,拒绝跨桶拼 batch
- 超时检测用 busy-wait +
rdtsc(x86)或clock_gettime(CLOCK_MONOTONIC_RAW)(Linux),避开 syscall 开销 - batch 合并后立即调用
reorder_kv_cache(),按 sequence length 升序排列,保证 memory access pattern 可预测
最易被忽略的一点:动态批处理的收益高度依赖请求到达的泊松分布特性。如果业务端是周期性脉冲(如每秒整点批量提交),batch 效果会断崖下跌——这时得切回静态 batch 或引入 jitter 机制打散时间戳。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











