翻译:你的Java后端经验,就是AI Infra最好的预习材料。
一、你先要理解一件事
AI Infra 不是什么全新的知识体系。它是一个同构系统,只是运行在 GPU 上。
你的舒适区 AI Infra 的真相
───────────────────────────────────────────────────────
Netty EventLoop 接收请求 SM(Streaming Multiprocessor)接收 Warp
ByteBuf 池化管理内存 PagedAttention 分块管理显存
epoll 事件驱动 IO CUDA Stream 异步执行
JVM 的 GC 分代回收 KV Cache 的 Block Eviction
Kafka 的分区与批量 NCCL 的 Ring AllReduce
Tomcat 的连接池 Triton Server 的 Request Queue
Spring 的 AOP 代理 CUDA Graph 的 Kernel 捕获
CompletableFuture 异步编排 CUDA Stream 的依赖管理
JMX / Metrics 监控 Nsight Systems 性能分析
一模一样的问题,一模一样的抽象层次,区别只在 Scale 和物理介质。
这篇文章不会给你画路线图。这篇文章会带你看真实的代码,看同一个问题在两个世界的不同解法。
二、ByteBuf 和 Tensor:同一个内存管理问题的两个版本
2.1 从一段 Netty 代码说起
// Netty 中你每天都在写的代码
ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(4096);
// 你分配了 4096 字节的堆外内存
buf.writeBytes(data);
// ...
buf.release(); // 引用计数减一,池里回收
这个模式对你来说稀松平常。但你有没有想过——这段代码和 vLLM 分配一块 GPU 显存放在本质上是同一件事?
Netty PooledByteBufAllocator vLLM Block Manager
─────────────────────────────── ────────────────────
arena(内存区域) GPU 显存池(预分配)
chunk(大块内存,16MB) GPU block(每个 16KB-64KB)
page(内存页,8KB) token block(每个 16 个 token)
PoolThreadCache(线程级缓存) Block Table(每个请求的页表)
引用计数(refCnt) Block 引用计数(共享 prefix cache)
2.2 看看 vLLM 的源码
# vllm/block_manager.py — 简化后的核心逻辑
class BlockAllocator:
"""这和 Netty 的 PoolArena 是同一类设计。"""
def __init__(self, num_blocks: int):
# 预分配所有 block,类似 Netty 的 PoolChunkList
self.free_blocks = list(range(num_blocks))
self.refcounts = [0] * num_blocks # 引用计数,和 ByteBuf.refCnt() 一样
def allocate(self) -> int:
"""从空闲列表拿一个 block。"""
block = self.free_blocks.pop()
self.refcounts[block] = 1
return block
def free(self, block: int):
"""释放并回收。"""
self.refcounts[block] -= 1
if self.refcounts[block] == 0: # 参考 ByteBuf.release() 的 return true
self.free_blocks.append(block)
def allocate_mutiple(self, num_blocks: int) -> List[int]:
"""批量分配。和 PooledByteBufAllocator 的 allocate() 一样走批量路径。"""
return [self.allocate() for _ in range(num_blocks)]
你的 Netty 知识直接等价:
你在用 PooledByteBufAllocator 时已经理解的事情:
- 预分配比按需分配快 — vLLM 启动时预分配所有显存 block
- 引用计数避免内存泄漏 — ByteBuf 的
refCnt()= block 的refcounts[] - 按大小分层管理 — Netty 的 Tiny/Small/Normal/Huge = vLLM 的 token block / sequence block / swap block
- ThreadLocal 减少竞争 — Netty 的
PoolThreadCache= vLLM 的 per-sequence block table
不同在哪:
- vLLM 要处理 GPU 显存的碎片化——GPU 没有 compacting GC,碎片化是显存管理的头号难题
- Netty 的 ByteBuf 可以 resize,GPU 显存分配后不能动态增长
- GPU block 释放要等 kernel 执行完(异步),ByteBuf 释放是同步的
2.3 生产中的真实问题
你在 Java 后端的生产环境可能遇到过:
“Full GC 后老年代降不下来,MAT 分析发现是 ByteBuf 引用没释放”
vLLM 部署时完全一样的场景:
“推理服务跑了 2 小时后 OOM(显存溢出)。排查发现某个请求异常断开后,对应的 KV Cache block 的引用计数没有归零,Block Manager 永远不会回收这些 block”
排查思路完全相同:
- 定位所有引用的持有者(Java: GC Root → GPU: reference graph)
- 追踪引用链条(Java: heap dump → GPU: block table dump)
- 找到未释放的真实原因(Java: 回调没执行 → GPU: 客户端 gRPC 流异常断开)
2.4 面试视角
面试题:“vLLM 的 PagedAttention 显存管理是怎么解决碎片化问题的?”
你可以这样回答(面试官知道你是 Java 背景,他期待看到的是类比能力):
“它本质上在做一件事——把 GPU 显存从 JVM 的连续堆模式改成操作系统的分页模式。
类比我们的 Netty 应用:如果我们给每个请求分配一大块连续堆外内存,并发多了就容易碎片化。解决方案是预分配固定大小的 block,用 block table 做虚拟地址到物理地址的映射——这就是 PagedAttention。
具体做法:
- 所有显存预分成 16KB 的 block(类似 Netty 的 PoolChunk 分 page)
- 每个请求维护一个 block table(类似虚拟内存的页表)
- 逻辑上连续的 KV Cache,物理上可以是离散的 block
- Block 复用和 Copy-on-Write 共享 prefix(类似 JVM 的 String.intern)
这样显存利用率从 40% 提升到了 95% 以上。关键手段是消除外部碎片——这和 JVM 的 G1GC 用 Region 解决 CMS 碎片问题是同一个思路。”
三、epoll → CUDA Stream:事件驱动的异步执行
3.1 先看 epoll
你使用 Netty 时,NioEventLoop 的 main loop 是这样的:
// Netty 的 NioEventLoop.java — 简化版
while (true) {
// 1. 等待事件
selector.select(); // ← epoll_wait()
// 2. 处理 IO 事件
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
// 根据事件类型分发
if (key.isReadable()) handleRead(key);
if (key.isWritable()) handleWrite(key);
if (key.isAcceptable()) handleAccept(key);
}
// 3. 处理定时任务
runAllTasks(timeout);
}
这个模式叫事件驱动循环(event-driven loop)。你太熟悉了可能不觉得有什么特别。
3.2 但这就是 CUDA Stream 的核心模型
// CUDA Stream — 注意这是一回事
cudaStream_t stream;
cudaStreamCreate(&stream);
while (true) {
// 1. 检查是否有 kernel 完成
cudaError_t err = cudaStreamQuery(stream); // ← 类似 epoll_wait 的非阻塞版
// 2. 如果当前 batch 完成,调度下一批
if (err == cudaSuccess) {
launch_next_batch(stream); // ← 类似 handleWrite
}
// 3. 没有新请求时,异步拷贝数据
cudaMemcpyAsync(dst, src, size, cudaMemcpyHostToDevice, stream);
// 这只是 push 到队列,不会阻塞
// 4. 检查有没有 callback 要执行
// cudaStreamAddCallback(stream, my_callback, nullptr, 0);
// ← 类似 runAllTasks
}
你要理解的关键:
selector.select()等待 IO 事件 =cudaStreamSynchronize()等待 GPU 完成handleRead()/handleWrite()处理不同事件 = 往不同 CUDA Stream 提交 kernelrunAllTasks处理定时任务 = CUDA callback 处理后处理逻辑
不同的技术栈,同一个抽象模型:事件循环 + 异步 IO + 任务队列。
3.3 更具体地看:为什么 CUDA Stream 就是 GPU 版的 EventLoop
# vLLM 推理引擎的核心循环 — vllm/engine/async_llm_engine.py
class AsyncLLMEngine:
async def run_engine_loop(self):
while True:
# 1. 等待新请求到来 ← 类似 selector.select()
request = await self.scheduler.wait_for_request()
# 2. 调度到 GPU 执行
result = await self._run_workers(
"execute_model", # ← 类似 handleRead 的角色
seq_group_metadata=request
)
# 3. 处理结果,发送响应
await self._process_result(result)
这就是一个加了 async/await 的 EventLoop。
如果你的 Netty 背景让你理解:
- 一个线程循环处理多个连接的 IO 事件
- 事件到来时触发回调
- 回调里不能做耗时操作(否则阻塞其他连接)
那你就已经理解了 GPU 推理的核心调度逻辑。
3.4 CUDA Stream 的并行
你还在 Java 里做过另一件事:
// 两个独立的异步任务并行执行
CompletableFuture.supplyAsync(this::fetchFromDB)
.thenCombine(CompletableFuture.supplyAsync(this::fetchFromCache),
(db, cache) -> merge(db, cache));
CUDA Stream 就是 GPU 版本的 CompletableFuture:
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// Stream 1: 数据传输
cudaMemcpyAsync(dst1, src1, size, H2D, stream1);
// Stream 2: 内核计算
myKernel<<<grid, block, 0, stream2>>>(dst2, src2);
// 两个 Stream 在 GPU 上并行执行!
// 等待两者都完成(类似 CompletableFuture.allOf)
cudaDeviceSynchronize(); // 类似 CompletableFuture.allOf().join()
Netty EventLoop 不能并行——因为它是单线程的。 CUDA Stream 天生可以并行——因为 GPU 有几百个 SM。
但模式完全一样:事件循环 + 异步任务 + 依赖编排。
四、Netty Reactor → GPU Warp Scheduler:同一个调度问题
4.1 Netty 的调度困境
Netty 里 EventLoop 是单线程的。所有 Channel 的 IO 事件在同一个线程上串行处理。
你遇到过这种问题:
// 问题:一个 Channel 的 handler 阻塞了
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Thread.sleep(1000); // 阻塞 1 秒
}
});
// 后果:在同一个 EventLoop 上的其他所有 Channel 都被阻塞了
解决思路:把 Channel 分散到不同的 EventLoop 上。
4.2 GPU 的 Warp 调度
GPU 上也有完全一样的命题——但它解决得更好。
GPU 的执行单位叫 Warp(32 个线程)。一个 SM(Streaming Multiprocessor)里有 4 个 Warp Scheduler,同一时刻只能发射一个 Warp 的指令。
但 GPU 有一个能力远超你的线程池:
Warp 切换零成本。
CPU 线程切换:保存寄存器(几百个字节) + 刷新 TLB + 内核态切换 ≈ 1000+ cycles
GPU Warp 切换:选另一个 Warp 发射下一指令 ≈ 0 cycles(寄存器已驻留)
原因: GPU 为每个 Warp 预分配了完整的寄存器文件。切换就是换一个程序计数器(PC)。
4.3 具体到代码
// 一个 CUDA Kernel
__global__ void compute(float* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= N) return;
// 这是一条指令,32 个线程同时执行
float val = data[idx];
// 如果当前 warp 里有的线程快有的线程慢了
// → GPU 直接切换到另一个 warp,让这个 warp 等着
// → 等这个 warp 的依赖满足了再切回来
// → 这个过程没有指令开销
data[idx] = val * 2.0f;
}
你写的 Netty handler 阻塞了 → 所有其他连接等着。 GPU 的一个 warp 等内存 → SM 切换到另一个 warp → 零成本。
这个差异背后就是 CPU 和 GPU 的根本哲学不同:
| CPU(你的线程池) | GPU(Warp Scheduler) | |
|---|---|---|
| 线程切换成本 | 高(几百 cycles) | 零 |
| 隐藏延迟方式 | 减少阻塞 | 用大量线程隐藏延迟 |
| 设计哲学 | 让每个线程尽可能快 | 让总体吞吐尽可能高 |
| 典型线程数 | 几十 | 几万到几十万 |
从这个角度看,GPU 设计就是"如果我把线程切换成本降为零,我该怎么调度"这个思想实验的工程实现。
4.4 一个让你豁然开朗的类比
// 你的线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
16, 32, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000)
);
// 100 个任务,每个任务可能 IO 阻塞
// 阻塞了 → 线程被占着 → 其他任务等 → 线程池满 → 拒绝策略触发
// 优化方向:异步化,减少阻塞
// GPU 的 "线程池"
// 16384 个线程(一个 SM 上的最大并发)
// 每个线程都可能等内存读取
// 等着呢 → warp scheduler 切到下一个 warp → 前面那个等着
// 只要你总线程数够多(occupancy 高),内存延迟就能被隐藏
// 优化方向:提高 occupancy,而不是减少等待
这是架构思想的根本差异:
你花了很多年学会"不要阻塞线程"。
GPU 的答案是:“阻塞就阻塞,再开几万个线程就行。”
五、Kafka 分区 → NCCL Ring AllReduce:同一个通信问题
5.1 从 Kafka 的 Rebalance 说起
你部署过 Kafka 消费者组:
Topic: "my-topic"(12 个分区)
消费者组:4 个消费者
→ 每个消费者处理 3 个分区
Rebalance 时:
停止消费 → 重新分配分区 → 恢复消费
(这个停顿你遇到过,通常几秒到几十秒)
5.2 分布式训练的 AllReduce
分布式训练时,NCCL 做梯度同步,和 Kafka 的分区消费是同一个抽象:
Topic = 所有 GPU 需要同步的梯度张量
│
├── Partition 0(梯度分片 0)→ GPU 0 负责 reduce
├── Partition 1(梯度分片 1)→ GPU 1 负责 reduce
├── Partition 2(梯度分片 2)→ GPU 2 负责 reduce
└── Partition 3(梯度分片 3)→ GPU 3 负责 reduce
NCCL 的 Ring AllReduce 算法:
Step 1 — Scatter-Reduce(类似 Consumer 拉取自己的分区)
GPU0: 分片 0 → GPU1,分片 1 → GPU2,分片 2 → GPU3,分片 3 → GPU0
每个 GPU 把自己持有的分片传到下一个 GPU,收到后做 reduce
Step 2 — AllGather(类似广播最新的 offset)
每个 GPU 把 reduce 后的完整分片沿 ring 广播给所有人
Kafka 消费者 → 每个处理一部分数据。 NCCL AllReduce → 每个 GPU 处理一部分梯度。
5.3 一个关键区别
Kafka 的分区分配是静态的(除非 rebalance)。
NCCL 的 Ring 是动态的,数据在环上转。
Kafka 消费者组:
[C0: P0,P1,P2] [C1: P3,P4,P5] [C2: P6,P7,P8] [C3: P9,P10,P11]
↑ 每个消费者只碰自己的分区
NCCL Ring:
GPU0 ──→ GPU1 ──→ GPU2 ──→ GPU3
↑ │
└────────────────────────┘
↑ 数据转一圈,每个 GPU 都接触所有数据
为什么 NCCL 用 Ring?因为 GPU 的 NVLink 是点对点高速连接(600GB/s),不需要中心交换机。Ring 在点对点拓扑上效率最高。
这和分布式系统设计的原则完全一致:通信拓扑要匹配物理拓扑。
六、JVM JIT → TensorRT:编译优化的两个版本
6.1 JIT 在 JVM 里做了什么
JVM 的 C2 编译器做了这些优化:
- 内联:把被频繁调用的方法体直接嵌入调用处
- 循环展开:减少循环控制开销
- 逃逸分析:判断对象是否逃逸出线程,决定是否栈上分配
- 锁消除:检测到锁不会竞争,直接去掉
6.2 TensorRT 在 GPU 上做了什么
TensorRT 对一个推理计算图做的优化:
# 优化前 — 多个独立的 CUDA Kernel
def forward(x):
# Kernel 1: 卷积
x = conv2d(x, weight, bias)
# 把全部 128MB 结果写回 HBM
# Kernel 2: ReLU 激活
x = relu(x)
# 再读 128MB,写 128MB
# Kernel 3: BatchNorm
x = batchnorm(x)
# 再读 128MB,写 128MB
# TensorRT 优化后 — 合并成一个 Kernel(类似 JVM 内联)
# conv + relu + batchnorm 在一个 Kernel 里完成
# 中间结果在 SRAM 里直接传递,不需要写回 HBM
TensorRT 的算子融合 = JVM 的方法内联。 都是消除多余操作。
| JVM C2 优化 | TensorRT 对应 |
|---|---|
| 方法内联 | 算子融合(Kernel Fusion) |
| 死代码消除 | 未使用的输出裁剪 |
| 循环展开 | 调整 grid/block 大小 |
| 逃逸分析 → 栈上分配 | 消除中间 Tensor(HBM 读写 → SRAM) |
| 窥孔优化 | 特定 GPU arch 的指令选择 |
| 分层编译(解释 → C1 → C2) | INT8 校准 → FP16 → TF32 |
6.3 具体到源代码
想理解 TensorRT 的算子融合,先看一段 JIT 日志:
// JVM JIT 日志
// -XX:+PrintCompilation
// -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
@ 20 java.lang.String::hashCode (67 bytes) inline (hot)
@ 28 java.lang.String::isLatin1 (19 bytes) inline (hot)
@ 34 java.lang.StringLatin1::hashCode (42 bytes) inline (hot)
然后看 TensorRT 的融合日志:
[TensorRT] VERBOSE: Layer fusion:
Fusing (conv2d_0) + (relu_1) → CBR_0
Fusing (CBR_0) + (pool_2) → CBR_Pool_0
Eliminated tensor: conv2d_0_output (128 MB) ← 省了一次 HBM 读写
Eliminated tensor: relu_1_output (128 MB) ← 又省了一次 HBM 读写
Total HBM savings: 256 MB per inference step
你不需要重新学编译器。你只需要把 JIT 知识迁移到 GPU 上下文。
6.4 工程价值
我在生产环境见过最离谱的事情:
一个团队花了两周优化推理延迟,各种调 PyTorch 代码,效果不明显。后来用 TensorRT 优化了下计算图——INT8 量化 + 算子融合,延迟直接降了 60%。没人花时间看 TensorRT 的优化选项,因为他们不懂"我的计算图为什么可以优化"。
如果你理解 JIT 的作用,你会知道:
- TensorRT 的 profile 模式和 JIT profiling 一样——需要跑 warmup 数据
- 你的模型结构影响融合效果——就像代码结构影响 JIT 内联
- 有些融合可能劣化——就像有些内联让 code cache 溢出
- 冷启动问题——就像 JIT 需要预热一样,TensorRT engine 第一次加载也要编译
七、一个完整的生产案例:从 Java 后端视角看推理服务部署
我们来走一个真实的推理服务部署的全流程,全部用你的 Java 经验透视。
7.1 架构
客户端(HTTP/gRPC请求)
↓
Triton Inference Server(类比:Spring Cloud Gateway)
↓ 路由到不同模型
vLLM 实例(多模型,类似多个 Tomcat 实例)
↓ 每层 Transformer 的 GPU 计算
GPU(A100 × 8,类似 8 核 CPU 的超集)
7.2 请求到达Triton
# 你的 Spring Boot 经验:
# @PostMapping("/predict")
# public Prediction predict(@RequestBody Request req) {
# return modelService.predict(req);
# }
# Triton 的等价格:
# 配置 triton/model_repository/gpt/1/config.pbtxt:
name: "gpt"
backend: "vllm"
max_batch_size: 64
input [
{ name: "input_ids", data_type: TYPE_INT64, dims: [-1] }
]
output [
{ name: "output_ids", data_type: TYPE_INT64, dims: [-1] }
]
instance_group [{ count: 4 }] # ← 4 个模型实例,类似 4 个 Tomcat worker
你不需要学新架构。这就是 API 网关 + 负载均衡 + 多实例。
7.3 vLLM 收到请求后的处理
# 这对你来说应该很熟悉了
class AsyncLLMEngine:
def __init__(self):
self.scheduler = Scheduler() # ← 类似 Disruptor 的 Sequencer
self.block_manager = BlockAllocator() # ← 类似 ByteBuf 池
self.worker = ModelWorker() # ← 类似业务 Service
self.request_queue = asyncio.Queue() # ← 类似 MQ
async def add_request(self, request):
await self.request_queue.put(request) # 放到队列
# 和 Kafka producer.send() 一样:投递完就返回
async def step(self):
"""每一步推理——类似你的定时任务调度"""
# 1. 从队列取请求
requests = self.scheduler.schedule() # ← 类似 MQ poll batch
if not requests:
return
# 2. 准备输入数据的元数据
# 类似 MyBatis 的 SQL 构建
metadata = self._prepare_inputs(requests)
# 3. 执行推理 — 实际走 CUDA kernel
# 类似 JDBC execute
outputs = self.worker.execute_model(metadata)
# 4. 发送结果 — 类似 HTTP 响应
await self._send_outputs(outputs)
这个循环和你写的 Kafka Consumer + 数据库写入的异步生产者-消费者代码,结构上没有区别。
7.4 生产中的真实调优经验
你在 Java 后端调优时遇到过:
“QPS 上不去,发现是线程池太小,任务都在队列里等”
vLLM 的等价问题:
“推理吞吐上不去,发现是
max_num_batched_tokens太小,GPU 没吃饱”
你在 Java 后端调优时遇到过:
“接口响应慢,发现是数据库连接池不够,大多数时间在等连接”
vLLM 的等价问题:
“推理延迟高,发现是显存 block 不够,请求在等 block 释放”
你在 Java 后端调优时遇到过:
“GC 停顿导致接口毛刺,长尾延迟严重”
vLLM 的等价问题:
“一个请求产生大量 padding token,GPU 利用率震荡,推理时间不稳定”
调优的思路、方法和工具链不一样了,但问题本质完全一致。
八、从内存屏障到同步原语:同一个并发问题
8.1 Java 里的内存屏障
你写并发代码时依赖 volatile 和 happens-before:
volatile boolean ready = false;
// volatile 写入 → 内存屏障 → 其他线程立即可见
// 或者用 Unsafe 直接操作
Unsafe.putOrderedObject(arr, offset, value); // StoreStore 屏障
8.2 CUDA 里的内存屏障
一样的,只是名字叫 __syncthreads() 和 __threadfence():
__global__ void kernel(float* data) {
__shared__ float cache[256];
int idx = threadIdx.x;
cache[idx] = data[idx]; // 写入 shared memory
__syncthreads(); // ← 这是一个全 barrier
// 保证:所有线程对 shared memory 的写入都已完成
// 等价于:Java 的 volatile write + happens-before
// 现在才能安全读取其他线程写的数据
float sum = cache[0] + cache[1]; // 不会读到脏数据
}
你的 Java 并发知识在这里完全适用:
volatile read= 从主存(HBM)读到寄存器volatile writeStoreLoad 屏障 =__threadfence()保证全局可见synchronizedblock =__syncthreads()+ 隐式 fence(块内所有线程加入)CAS操作 =atomicAdd()/atomicCAS()(GPU 版本)
九、你的第一个 CUDA Kernel:从 Java Stream 开始
9.1 你熟悉的
// Java 中你对集合做操作
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
list.parallelStream()
.map(x -> x * 2)
.filter(x -> x > 5)
.collect(toList());
// 结果: [6, 8, 10, 12, 14, 16]
9.2 CUDA 版本
// CUDA Kernel
__global__ void process(int* in, int* out, int* count, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= N) return;
// 这就是你的 lambda: x -> x * 2
int val = in[idx] * 2;
// 这就是你的 filter: x -> x > 5
if (val > 5) {
// GPU 版本的 filter → collect
// atomicAdd 就像并行 stream 里对共享容器的并发 add
int pos = atomicAdd(count, 1);
out[pos] = val;
}
}
// 启动 Kernel — 这就是你的 parallelStream()
// 1 block × 8 threads = 8 个并行线程
process<<<1, 8>>>(d_in, d_out, d_count, 8);
Java Stream 的 parallelStream() 和 CUDA Kernel 的 <<<grid, block>>> 都是同一件事:把数据分散到多个线程并行处理。
区别只是 scale——CUDA 让你从 8 个线程变成 100 万个线程。
9.3 如果你现在就想写第一个 CUDA 程序
// vecAdd.cu — 向量加法,你的第一个 GPU 程序
#include <cuda_runtime.h>
#include <stdio.h>
__global__ void vecAdd(float* A, float* B, float* C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) {
C[i] = A[i] + B[i];
}
}
int main() {
int N = 1 << 20; // 1048576 个元素
float *h_A, *h_B, *h_C; // host (CPU) pointers
float *d_A, *d_B, *d_C; // device (GPU) pointers
// 分配 CPU 内存
h_A = (float*)malloc(N * sizeof(float));
h_B = (float*)malloc(N * sizeof(float));
h_C = (float*)malloc(N * sizeof(float));
// 分配 GPU 显存 — 像 Netty 的 directBuffer 分配,但在另一块物理内存上
cudaMalloc(&d_A, N * sizeof(float));
cudaMalloc(&d_B, N * sizeof(float));
cudaMalloc(&d_C, N * sizeof(float));
// 拷贝数据到 GPU — 像跨网络传输数据
cudaMemcpy(d_A, h_A, N * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_B, h_B, N * sizeof(float), cudaMemcpyHostToDevice);
// 启动 Kernel — 1024 blocks × 1024 threads = 1,048,576 线程
vecAdd<<<1024, 1024>>>(d_A, d_B, d_C, N);
// 等待 GPU 完成 — 像 Future.get()
cudaDeviceSynchronize();
// 拷贝结果回 CPU
cudaMemcpy(h_C, d_C, N * sizeof(float), cudaMemcpyDeviceToHost);
// 释放
cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
free(h_A); free(h_B); free(h_C);
return 0;
}
编译:
nvcc -o vecAdd vecAdd.cu
./vecAdd
十、为什么你应该现在就开始
10.1 一个不太好听的实话
AI Infra 领域的很多从业者其实不懂系统。
他们会用 PyTorch 写模型、会用 vLLM 部署服务,但:
- 不知道为什么显存用了几张卡就 OOM 了
- 不知道为什么
batch_size=32和batch_size=64的吞吐差距不是 2 倍 - 不知道为什么 RDMA 比 TCP 快那么多
- 不知道为什么
numactl绑了一下 GPU 推理就快了 20%
而这些问题对于系统工程师来说,都是基本功。
你还记得你第一次排查 Java 网络问题时的思路吗?
- 先看网络层有没有丢包
- 再看系统层 socket buffer 有没有满
- 再看应用层连接池有没有耗尽
- 最后定位到是 DNS 解析超时
这种系统级的问题排查能力,是 PyTorch 背景的工程师普遍欠缺的。
10.2 你能带来的价值
纯 AI 背景的工程师能:
- 用 PyTorch 写一个 attention 实现
- 在单卡上训一个模型
- 调参知道 loss 不收敛怎么办
你能带来的额外价值:
- 把推理服务当作高并发系统来设计
- 理解 KV Cache 就是内存池,知道怎么管理
- 知道 NCCL 通信的拓扑瓶颈,知道怎么优化
- 知道 NUMA 对数据传输的影响,知道怎么配置
- 能从系统层面诊断性能问题,而不是只看模型 LOSS
这不比只懂 PyTorch 值钱得多?
10.3 真实的市场信号
看看这些招聘岗位的要求:
AI Infra 工程师(某大厂,年薪 150W+)
- 精通 C++/Go/Rust/Python
- 理解 GPU 架构和 CUDA 编程
- 理解分布式系统和高性能网络
- 熟悉推理框架(vLLM/TensorRT/Triton)
- 加分项:有高并发中间件开发经验
加点粗的那行不就是你吗?
推理引擎工程师
- 能优化 Transformer 推理延迟
- 熟悉 TensorRT API
- 了解 GPU 内存层次和访存优化
- 有 CUDA Kernel 调优经验
- 加分项:有系统性能调优经验
你过去十年积累的经验——并发、分布式、性能分析、中间件——每一份都是"加分项"里的核心技能。
附录:立刻可以写的代码
写一个 Java 版本的 PagedAttention 模拟器
- 用 ByteBuf 模拟 GPU 显存 block
- 实现 allocate / free / reference counting
- 理解碎片化问题
- 200 行代码,全部是你已有的知识
写一个 Continuous Batching 模拟器
- 用 Java 线程池模拟 GPU
- 4 个 worker 线程 = 4 个 SM
- 实现"每轮迭代检查新请求"的调度逻辑
- 比较"等批次凑满" vs “持续追加"的吞吐差距
- 300 行代码,全部是 Disruptor + MQ 的知识
部署 vLLM 并加日志
export VLLM_LOGGING_LEVEL=DEBUG- 观察每个请求在每层 Transformer 的耗时
- 观察 Block Manager 的分配 / 释放
- 用
vllm/block_manager.py里加print - 这是你的"源码阅读"第一步
写一个小型 NCCL 模拟器
- 用 Java Socket 模拟 GPU 节点
- 实现 Ring AllReduce 的 Scatter-Reduce 和 AllGather
- 4 个进程,每个有 4MB 数据
- 看不同拓扑(Ring vs Tree vs Star)的通信时间
- 200 行 Socket + Thread 代码
这些项目证明:你不需要重学计算机科学,你只需要把现有的知识加上 GPU 这个新维度。
写出来的话,你就已经有了 AI Infra 工程师的思维模型。