大家好,我是内核喵。
最近不管刷推还是逛各种技术社区,铺天盖地全是“AI 重构一切”、“无脑上大模型”。但凡多问一句:“你们这套 Pipeline 在生产环境压测过吗?系统瓶颈卡在哪?”得到的回答往往是玄学——“GPU 算力不够”、“Python 太慢了”、“模型参数太大没法优化”。
作为一个习惯了万事挂 strace、没事跑 perf 的系统调优强迫症,今天我就从底层系统栈、内核以及系统工程的视角,来较真聊聊关于人工智能与大模型部署落地中的三大典型技术误区。掀开神秘的“AI 黑盒”,底层其实依旧是内存带宽、PCIe 总线、页表以及系统调用。
误区一:“跑大模型全靠 GPU,CPU 和 Host 内存就是个打杂的”
很多人配推理服务器时,动辄堆 8 张 A100/H100,CPU 却只舍得配几个便宜的入门核,Host 内存也不做 NUMA 绑定。他们的逻辑是:“矩阵乘法都在 Tensor Core 上算,CPU 只要把模型丢进去看戏就行。”
真相当场打脸: GPU 确实负责算 GEMM(通用矩阵乘法),但指令发射(Kernel Launch)、数据流水搬运(Data Pipeline)、以及动态序列的调度管理全在 CPU 上。
如果你用 perf top 观察过一个未经调优的高并发 LLM 推理服务,你会发现 CPU 的 cycles 经常全浪费在系统调用和内存锁竞争上:
# perf top -p $(pgrep -f vllm)
24.12% [kernel] [k] copy_user_enhanced_fast_string
18.05% [kernel] [k] page_fault
12.33% libcuda.so.1 [.] 0x00000000002a4b12
8.41% [kernel] [k] _raw_spin_unlock_irqrestore
如果你的输入文本预处理(Tokenizer)、动态 Batch 调度逻辑全在单核上硬抗,GPU 的 Compute Engine 大概率在频繁经历 GPU Starvation(饥饿等待)。
更要命的是内存拷贝。很多服务没有显式开启 Pinned Memory(锁页内存 / Page-locked Memory),导致每次 Host-to-Device 传输都要在内核态做一次中转拷贝:
# 错误示范:普通的 Host Memory 搬移,触发二次拷贝与页表缺页
tensor = torch.from_numpy(raw_data).to("cuda")
# 正确姿势:分配 Non-pageable 内存,允许 DMA 直接走 PCIe 直达显存
tensor = torch.from_numpy(raw_data).pin_memory().to("cuda", non_blocking=True)
如果你用 strace -c -f -p <pid> 抓一下没做 Pinned Memory 的进程,会看到密密麻麻的 ioctl(调用 /dev/nvidia*)和昂贵的系统缺页异常:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
58.12 1.240102 12 103340 ioctl
21.05 0.449100 45 9980 sched_yield
11.20 0.239012 8 29881 futex
CPU 弱鸡或者 NUMA 节点跨跨连访,PCIe 带宽直接砍半,显卡利用率(SM Activity)掉到 30% 以下,买再贵的 GPU 也是在机房吹冷气。
误区二:“Python 太慢了,所以 AI 推理延迟高是 Python 的锅”
“Python 有 GIL,Python 是解释执行,所以我们要用 C++ 重写推理引擎才能降延迟。”这是后端工程师最爱提的伪需求之一。
真相是:脱离 Profiling 谈语言性能全是耍流氓。
在标准的深度学习推理中,Python 仅仅扮演了胶水层(Orchestrator)。当你执行一个 PyTorch 的 C = A @ B 时,Python 解释器只消耗了几微秒来调用底层预编译好的 C++/CUDA 动态链接库(比如 cuBLAS、Cutlass)。
真正拖慢延迟的,往往是算子碎片化(Operator Fragmentation)引发的 Launch Overhead。
看下面这个简化示例:
# 坏味道:连续调用小算子,每一个都在向 GPU 提交小型任务队列
def naive_op(x, bias):
x = x + bias # 触发一次 Element-wise Add Kernel
x = torch.relu(x) # 触发一次 Activation Kernel
x = x * 1.5 # 触发一次 Scalar Mul Kernel
return x
拿 perf trace 或者 NVIDIA Nsight Systems 抓一下时间线,你会发现 GPU 执行这些微小 Kernel 只需要 1 微秒,但 Host 端准备发射参数、下发队列、处理 Driver API 的开销花了 5 微秒。计算时间远远小于指令发射开销。
这时候换 C++ 也没用,正确的解法是算子融合(Kernel Fusion),让一次计算直接吃满 SRAM/Shared Memory,减少全局显存(HBM)读写和系统调用:
# 使用 TorchScript / Inductor / Triton 进行图编译和算子融合
@torch.compile(mode="reduce-overhead")
def fused_op(x, bias):
return torch.relu(x + bias) * 1.5
底层被编译成单个 Triton/CUDA Kernel 后,多次 IO 变成了芯片内部寄存器流转,调度瓶颈迎刃而解。别甩锅给 Python 的解释器,先看看你的 Kernel Launch 是否把 CUDA Stream 给塞爆了。
误区三:“显存只要没 OOM,Batch Size 越大吞吐量就线性上涨”
做过高并发后端的人都知道连接池、批处理能提升效率,于是很多部署工程师把 LLM 推理的并发 Batch Size 设得极大,心想:“反正 80GB 显存还剩 20GB,不 OOM 就算胜利,吞吐一定更高。”
系统调优常识:计算受限(Compute-Bound)与访存受限(Memory-Bound)的临界点。
LLM 的自回归生成(Decode 阶段)本质上是一个极度 Memory-Bandwidth Bound(显存带宽受限) 的任务。每次生成一个 Token,都需要把整个模型权重视角以及前面的 KV Cache 从显存重新加载一次。
当你的 Batch Size 增加时:
- 显存碎片化加剧:没有 vLLM 那样的 PagedAttention 机制,传统的连续内存分配会引发类似 Linux 物理内存碎片的问题,大量的 Virtual Memory 映射变成不可用孔洞。
- Cache 命中率雪崩:大 Batch 导致 L2 Cache 根本容纳不下当前步的上下文,GPU SM 单元大量停摆等待 HBM 数据搬运。
我们跑一个简单的测试,观察 Prefill 阶段(算力受限)与 Decode 阶段(访存受限)的性能表现,吞吐量(Tokens/sec)绝不是一条笔直的线性曲线:
Batch Size = 1 -> Latency: 15ms/tok | Throughput: 66 tok/s
Batch Size = 8 -> Latency: 18ms/tok | Throughput: 444 tok/s (提升显著,利用了空闲 SM)
Batch Size = 32 -> Latency: 42ms/tok | Throughput: 761 tok/s
Batch Size = 128 -> Latency: 220ms/tok | Throughput: 581 tok/s (崩盘!显存带宽打满,排队时延反噬)
如果盲目堆大 Batch,你的服务 P99 延迟会直奔几秒甚至十几秒而去,用户端直接超时断连。系统架构的目标永远不是榨干单一指标,而是在时延(SLA)与吞吐(Throughput)之间寻找帕累托最优解。
总结:请像对待高并发系统一样对待 AI
AI 不是魔法,它的背后依旧是经典的计算机系统体系结构(von Neumann 瓶颈、总线带宽、页表与中断、内存层次结构)。
别再停留在“调模型参数靠玄学、调推理性能靠砸钱”的阶段了。遇到性能问题:
- 先拿
dmesg看看有没有 OOM Killer 踢掉进程; - 跑一跑
perf stat看看 IPC(每周期指令数)和 Cache Miss; - 上
strace盯一下驱动层是不是在频繁同步阻塞等待; - 抓一抓 GPU Profile,分清楚到底是 Bound 在 ALU 还是 Bound 在总线。
少一点营销概念,多拉两次火焰图,系统调优永远讲究真凭实据。我是内核喵,我们下期见!
License: CC BY-NC 4.0
Updated 13 hours ago
Was this article helpful? Give it a like.
0 comments


