3.8 推理引擎与生产实践(Engines & Production Practice)¶
本章你将学到
- vLLM、SGLang、TensorRT-LLM、llama.cpp、MLC-LLM 和 TGI 分别是什么、是怎么构建的,以及什么时候该选哪一个。
- 如何诚实地对一个推理服务系统做基准测试:负载形态、并发扫描、TTFT/TPOT/ITL 的百分位数、goodput——并附带一个可运行的基准测试客户端。
- 如何做容量规划(capacity planning):利用 Little 定律和实测的工作点,估算在延迟 SLO 下达到目标 QPS 需要多少 GPU。
- 如何计算每百万 token 的成本,以及哪些因素决定它。
- 服务推理模型和服务 RL rollout 时有什么不同(权重同步、异步 rollout、确定性与 batch 不变性)。
为什么重要¶
3.1–3.7 章讲解了各种机制。实践中你很少自己去实现它们:你要选一个引擎、配置它、测量它,然后决定买或租多少 GPU。现实中大多数错误都出在这里——基准测试与线上流量毫不相似、基于平均延迟做的容量规划、假设 100% 利用率的成本估算,或者推理引擎悄悄地与训练器产生分歧的 RL 流水线。本章就是从"理解"通向"运维"的桥梁。
直觉¶
推理引擎是一个运行时(调度器 + 内存管理器 + kernel,见 3.3–3.4 章),外面再包一层 API 服务器。引擎之间的差异主要体现在:(1) 面向哪些硬件,(2) 给 CPU 带来多少调度开销,(3) 哪些模型和特性(量化格式、投机方法、并行模式)已经可以直接使用,以及 (4) 修改起来有多容易。
基准测试是一次实验:负载是自变量,延迟百分位数和吞吐是因变量。容量规划就是把实验结果去除以你的需求,再加上余量。成本则是 GPU 小时数除以所服务的 token 数。
工作原理¶
引擎巡礼¶
| 引擎 | 它是什么 | 什么时候选它 |
|---|---|---|
| vLLM | 基于 Python/PyTorch 的引擎,首创 PagedAttention;支持连续批处理(continuous batching)、前缀缓存、分块预填充、多种量化和投机方法、TP/PP/EP,以及兼容 OpenAI 的服务器(vllm serve)。其 V1 引擎(2025 年起为默认)把 API 前端与一个忙循环的引擎核心分离,以降低 CPU 开销。 |
你想要最广的模型和硬件支持(NVIDIA、AMD、TPU,以及通过插件支持的其他硬件)、庞大的社区,以及从 Hugging Face checkpoint 出发的便捷路径。一个稳妥的默认选择。 |
| SGLang | 围绕 RadixAttention 前缀缓存构建的 Python 引擎,拥有低开销的调度器(让 CPU 调度与 GPU 工作重叠)、快速的语法约束解码,以及对大型 MoE 的强力支持(DeepSeek 式 EP、PD 分离)。 | 前缀密集的负载(agent、多轮对话、few-shot)、结构化输出、大型 MoE 部署、RL rollout(被广泛嵌入各 RL 框架)。 |
| TensorRT-LLM | NVIDIA 开源的 LLM 运行时,带手工优化的 kernel、in-flight batching、Hopper/Blackwell 上的 FP8/FP4;常部署在 Triton Inference Server 或 Dynamo 之后。 | 纯 NVIDIA 的集群,并且在最新 GPU 上追求极致性能,同时你能承受更复杂的构建/配置过程。 |
| llama.cpp / Ollama | C/C++ 推理,采用 GGUF 格式和多种量化类型;可运行在 CPU、Apple Silicon(Metal)、CUDA、Vulkan 等之上。Ollama 在其外面包了模型下载和本地 API。 | 笔记本、台式机、边缘设备、CPU 服务器、单用户本地使用。并非为高并发的数据中心服务而设计。 |
| MLC-LLM | 基于编译器的部署方案(Apache TVM):把模型编译到多种后端,包括手机以及通过 WebGPU 运行的浏览器。 | 端侧和浏览器内推理、特殊的硬件目标。 |
| Hugging Face TGI | 生产级服务器,用 Rust 写的路由器加模型分片,与 HF 生态集成。 | 你已经深度使用 HF 工具链;采用之前,请对照 vLLM/SGLang 检查项目当前的活跃度和特性覆盖。 |
引擎迭代很快——这个季度还是"SGLang 独有"的特性,下个季度可能就进了 vLLM,反之亦然。要用在你自己的模型、硬件和流量上跑出来的基准测试来做决定,而不是看特性清单。
真正重要的配置项¶
每个引擎都把 3.2–3.7 章的机制暴露成设置项。下面用的是 vLLM 的名字(vllm serve --help);SGLang 和 TensorRT-LLM 有名字不同的对应项。
| 设置项(vLLM) | 对应机制 | 调高它的效果 |
|---|---|---|
--tensor-parallel-size |
TP 度(3.7) | 总带宽和 KV 空间更多;all-reduce 开销更大 |
--max-model-len |
每个请求的最大上下文(3.2) | 支持更长的请求;能同时容纳的请求变少 |
--gpu-memory-utilization |
引擎可使用的 GPU 显存比例;放完权重之后剩下的部分成为 KV 池(3.3) | KV 块更多 → batch 更大;给其他进程留的余量更少 |
--max-num-seqs |
每个 batch 的最大序列数(3.3) | 在 KV 用完之前吞吐更高;TPOT 更高 |
--max-num-batched-tokens |
每步分块预填充的 token 预算(3.3) | 长 prompt 的 TTFT 更低;正在运行的解码的 ITL 更高 |
--enable-prefix-caching |
基于哈希的前缀缓存(3.3) | prompt 共享前缀时,TTFT 和预填充开销更低 |
--kv-cache-dtype fp8 |
KV 量化(3.2, 3.6) | KV 容量约 2 倍;有小幅精度风险 |
--quantization |
权重/激活格式,例如 FP8 或 AWQ(3.6) | 显存和带宽占用更少;精度必须检查 |
--speculative-config |
草稿方法和投机 token 数(3.5) | 低负载下延迟更低;高负载下可能有害 |
正确地做基准测试¶
1. 定义负载形态。 要测量或假设的是分布,而不是单个值:
- 输入长度和输出长度的分布(例如聊天:约 1–2K 输入 / 约 300 输出;RAG:约 8K 输入 / 约 200 输出;推理:约 1K 输入 / 8K+ 输出);
- 前缀结构——每个 prompt 中有多少是共享的(system prompt、文档、历史),这决定了前缀缓存的命中率;
- 到达过程——开环(open-loop,请求按某个速率到达,例如泊松过程)还是闭环(closed-loop,固定数量的并发用户,上一个请求结束就发新请求)。
2. 扫描负载。 在逐步增大的并发(闭环)或请求速率(开环)下运行:例如 1、4、16、64、128、256。在每个点上记录
- TTFT 的 P50/P90/P99、TPOT 的 P50/P99、ITL 的 P99(卡顿)、E2E 的 P99;
- 每秒输出 token 数和每秒请求数;
- goodput:满足所有 SLO 的每秒请求数。
画出延迟百分位数相对于吞吐的曲线。结果是一条曲线:一开始延迟是平的,随着系统饱和而急剧上扬。可用的工作点,是 P99 仍满足 SLO 的最高吞吐。
3. 控制变量。 测量前先预热服务器(CUDA graph 捕获、各种缓存);在客户端测量(用户体验到的,包括网络和 detokenization);比较不同引擎时固定输出长度(例如用 ignore_eos 选项,并明确说明)或使用真实输出;使用相同的 tokenizer 和相同的采样设置;重复运行。引擎都自带这类工具——vllm bench serve 和 SGLang 的 python -m sglang.bench_serving,可在合成或数据集 prompt 上扫描请求速率或并发(当前支持的参数请查看 --help)。
容量规划(Capacity planning)¶
Little 定律描述了任何稳定系统中负载与延迟的关系:系统中的平均请求数 \(N\),等于到达率 \(\lambda\) 乘以每个请求在系统中停留的平均时间 \(W\),
用大白话说:如果每秒到达 20 个请求,每个请求端到端耗时 14 s,那么任一时刻大约有 280 个请求在飞行中。这告诉你集群必须支撑的并发数。流程如下:
- 从基准测试中找出单个副本的工作点:在 P99 TTFT 和 TPOT 满足 SLO 的前提下,基于你的流量形态(其中包含预填充负载)所能达到的最大并发 \(C_{\text{rep}}\)(或吞吐 \(X_{\text{rep}}\),单位 output token/s)。
- 计算需求:output token/s \(= \lambda \cdot \bar N_{\text{out}}\),飞行中的请求数 \(= \lambda \cdot W\),其中 \(W \approx \text{TTFT} + \bar N_{\text{out}} \cdot \text{TPOT}\)。
- 所需副本数 \(= \max\big(\text{demand tokens/s} / X_{\text{rep}},\ \text{in-flight} / C_{\text{rep}}\big)\),向上取整,再加上应对峰值、故障和发版的余量(通常 20–50%)。
- 对飞行中的请求,按其长度,核对每个副本的 KV 显存是否够用(见 3.2 章)。
每百万 token 的成本¶
用大白话说:你每小时付出的钱,除以你每小时实际服务了多少百万 token。有三件事主导它:在你的 SLO 下的单 GPU 吞吐(延迟要求越紧 → batch 越小 → 成本越高)、集群在一天中的利用率(闲置的 GPU 照样花钱),以及输入 token 与输出 token 的计数方式(预填充 token 比解码 token 便宜得多,这也是 API 里输入 token 价格更低、命中缓存的输入 token 更低的原因)。
服务推理模型¶
推理模型每次回答会输出几千到几万个"思考" token。后果是:
- 解码占主导。 在 30 ms TPOT 下,一条 10,000 token 的推理链要花 5 分钟。决定用户感知延迟的是 TPOT 而不是 TTFT,每一项解码优化(FP8/FP4 权重、投机解码、MLA/GQA)都更重要。
- KV 在生成过程中增长。 长输出会在几分钟里持续占用 KV 显存;并发必须按最终长度来规划,如果放进来的请求过多,抢占(preemption,见 3.3 章)就会变得频繁。
- 长尾长度让静态的容量估算不可靠;要用输出长度的分布来规划,而不是均值。
- 产品层面的开关: 最大思考预算、effort 档位和提前退出,都是在质量与成本之间做权衡;引擎提供解析器,把推理内容与最终答案在 API 响应中分开。
为 RL rollout 提供服务¶
在 RL 后训练(第 2 部分)中,大部分墙钟时间花在生成 rollout 上。verl 和 slime 这样的框架把 vLLM 或 SGLang 嵌入为 rollout 引擎。这种用法有三个特有的问题:
- 权重同步。 每次优化器步之后,推理引擎都必须收到新权重:通过 NCCL 从训练器广播到推理 GPU;或者当训练和推理共用同一批 GPU("colocated")时,通过在两者之间交换显存来实现(引擎提供释放并重新获取 GPU 显存的模式)。同步一个 70B 的 BF16 模型要搬运约 140 GB,所以同步耗时很关键。
- 长尾与异步。 在同步的一步里,最慢的那条 rollout(最长的推理链)会拖住整个 batch。异步流水线让下一个 batch 的生成与当前 batch 的训练重叠,partial rollout 则把特别长的生成暂停,稍后再恢复。两者都会让数据略微变成离策略(off-policy)的(由更旧的权重生成),RL 算法必须容忍或修正这一点。
- 训练—推理不一致与确定性。 rollout 引擎计算 token 概率时用的 kernel 与训练器不同(而且精度往往更低),所以"同一个"策略给出的 log-probability 会略有差异,这可能让 RL 训练不稳定。Thinking Machines 的 Defeating Nondeterminism in LLM Inference 分析了一个根本原因:缺乏 batch 不变性(batch invariance)——浮点加法不满足结合律,而 kernel 会根据 batch 大小和序列长度选择不同的归约策略(split-K 矩阵乘、split-KV 注意力、不同的 tile 大小)。因此一个请求的数值结果取决于 batch 里还有什么别的请求。batch 不变的 kernel 无论 batch 如何组成都固定归约顺序,能得到可复现的输出,代价是一些性能。
代码¶
一个最小的异步基准测试客户端,适用于任何兼容 OpenAI 的服务器(vLLM、SGLang、通过其 OpenAI 服务器接入的 TensorRT-LLM、TGI)。它运行一个闭环的并发档位,并报告输出吞吐、TTFT/TPOT/ITL 百分位数和 goodput。在多个并发档位上各跑一次,就能描绘出延迟—吞吐曲线。
import asyncio, time, statistics
from openai import AsyncOpenAI # pip install openai;适用于任何兼容 OpenAI 的服务器
async def one_request(client, model, prompt, max_tokens):
t0 = time.perf_counter()
first, last, gaps, n_out = None, None, [], 0
stream = await client.completions.create(
model=model, prompt=prompt, max_tokens=max_tokens, stream=True,
stream_options={"include_usage": True})
async for chunk in stream:
now = time.perf_counter()
if chunk.choices and chunk.choices[0].text:
if first is None:
first = now # 第一个 token 到达
else:
gaps.append(now - last) # token(chunk)间延迟
last = now
if chunk.usage: # 最后一个 chunk 带有计数
n_out = chunk.usage.completion_tokens
e2e = time.perf_counter() - t0
ttft = first - t0
tpot = (e2e - ttft) / max(n_out - 1, 1)
return dict(ttft=ttft, tpot=tpot, e2e=e2e, n_out=n_out, gaps=gaps)
def pct(xs, p):
return statistics.quantiles(xs, n=100, method="inclusive")[p - 1]
async def run(base_url, model, prompts, concurrency, max_tokens=300,
slo_ttft=2.0, slo_tpot=0.05):
client = AsyncOpenAI(base_url=base_url, api_key="EMPTY")
sem = asyncio.Semaphore(concurrency) # 闭环:始终有 N 个请求在飞行中
async def guarded(p):
async with sem:
return await one_request(client, model, p, max_tokens)
t0 = time.perf_counter()
res = await asyncio.gather(*(guarded(p) for p in prompts))
wall = time.perf_counter() - t0
ttft, tpot = [r["ttft"] for r in res], [r["tpot"] for r in res]
itl = [g for r in res for g in r["gaps"]]
good = sum(r["ttft"] <= slo_ttft and r["tpot"] <= slo_tpot for r in res)
print(f"concurrency={concurrency:4d} out tok/s={sum(r['n_out'] for r in res) / wall:8.0f} "
f"TTFT p50/p99={pct(ttft, 50):.2f}/{pct(ttft, 99):.2f}s "
f"TPOT p50/p99={1000 * pct(tpot, 50):.0f}/{1000 * pct(tpot, 99):.0f}ms "
f"ITL p99={1000 * pct(itl, 99):.0f}ms goodput={good / wall:.2f} req/s")
# 针对本地服务器的扫描示例,服务器例如用 `vllm serve <model>` 启动:
# prompts = [open("my_real_prompts.txt").read()] * 200 # 用你自己的流量形态
# for c in (1, 4, 16, 64, 128):
# asyncio.run(run("http://localhost:8000/v1", "<model>", prompts, c))
为什么 batch 不变性是个真问题,四行代码就能说明:同样的数以不同顺序求和,会得到不同的 float32 结果,而 kernel 会随 batch 大小改变求和顺序。
import numpy as np
x = np.random.default_rng(0).standard_normal(4096).astype(np.float32) * 1e3
one_pass = x.sum(dtype=np.float32) # 一种归约顺序
split_4 = sum(c.sum(dtype=np.float32) for c in np.split(x, 4)) # 分成 4 块的 “split-K”
print(one_pass, split_4, "equal:", one_pass == split_4)
实例演算¶
示意性数字
下面的基准测试结果和 GPU 价格都是假设的输入,选得比较合理;重点是方法。请换成你自己的实测值和价格。
目标。 为一个聊天产品服务 Llama-3-8B(BF16)。需求:峰值 \(\lambda = 20\) 请求/秒;平均 prompt 1,000 token(无共享前缀),平均输出 300 token。SLO:P99 TTFT ≤ 1 s,P99 TPOT ≤ 50 ms。
第 1 步——对单个副本(1× H100)做基准测试,使用这种流量形态。假设扫描显示:并发为 128 时,副本能提供 2,800 output token/s,P99 TPOT 为 45 ms,P99 TTFT 为 0.8 s;并发为 192 时,P99 TPOT 超过 50 ms。工作点:\(C_{\text{rep}} = 128\),\(X_{\text{rep}} = 2{,}800\) token/s。(一致性检查:128 条序列 ÷ 每 token 0.045 s ≈ 2,850 token/s。)
第 2 步——需求。
- 每秒输出 token:\(20 \times 300 = 6{,}000\)。
- 在系统中的停留时间:\(W \approx 0.5\ \text{s} + 299 \times 0.045\ \text{s} \approx 14\ \text{s}\);飞行中的请求数:\(N = 20 \times 14 = 280\)。
第 3 步——副本数。 按吞吐算,\(6{,}000 / 2{,}800 = 2.14\);按并发算,\(280 / 128 = 2.19\)。向上取整为 3,再加约 30% 的余量(\(2.19 \times 1.3 = 2.85\)),3 个副本依然足够。峰值时需要 3 张 H100。
第 4 步——KV 核对。 280 个飞行中请求 / 3 个副本 ≈ 每张 GPU 93 条序列,每条最长 1,300 token,每 token 128 KiB(见 3.2 章):\(93 \times 1300 \times 128\ \text{KiB} \approx 14.8\) GiB——远低于约 50 GiB 的 KV 预算。预填充负载:\(20 \times 1000 = 20{,}000\) prompt token/s,每个副本约 6,700——占了每张 GPU 相当大一部分时间,这就是基准测试必须使用真实 prompt 长度的原因。
第 5 步——成本。 假设价格为每 GPU 小时 USD 2.50。峰值时,3 张 GPU 每小时花费 USD 7.50,每小时服务 \(6{,}000 \times 3600 = 21.6\) M 输出 token(外加每小时 72 M 输入 token):
如果全天的平均流量是峰值的 40%,但你一直开着 3 张 GPU,那么实际成本为 \(0.35 / 0.4 \approx 0.87\) USD 每百万输出 token——利用率和 kernel 速度同样重要。自动扩缩容(见 3.7 章)、FP8 权重(字节更少 → 同样 TPOT 下 batch 更大)和前缀缓存(预填充更少)是压低成本的主要手段;把 TPOT SLO 放宽到 80 ms 也能让 batch 更大,降低每 token 成本。
实践中的做法(2025–2026)¶
- 大多数开源模型的部署使用 vLLM 或 SGLang;以 NVIDIA 为中心、想在最新 GPU 上追求极致性能的团队使用 TensorRT-LLM(常通过 Dynamo);本地和边缘使用则以 llama.cpp/Ollama 和 Apple Silicon 专用的运行时为主。
- 基准测试工具已内置在引擎里(
vllm bench serve、sglang.bench_serving),也有独立的基准测试项目发布跨引擎的对比——在相信它们的排名之前,先读一读它们的负载定义。 - 推理服务栈会暴露 Prometheus 指标(队列长度、KV 利用率、前缀缓存命中率、投机解码接受率、TTFT/TPOT 直方图);这些就是自动扩缩容和 SLO 告警的输入。
- 推理模型让 TPOT 和长输出下的 KV 容量成为核心的服务指标,并让投机解码和 FP8/FP4 成为标配。
- RL 框架(verl、slime 等)把 rollout 引擎当作一等公民的组件;异步与 partial rollout、快速权重同步以及 batch 不变 / 确定性推理,是贯穿 2025 年的活跃工程课题。
上线前检查清单
- 启动日志中的 KV 容量(token 数或块数)能支撑你规划的并发和真实长度。
- 在你自己的流量形态上做的并发扫描显示,峰值需求加余量下 SLO 依然满足。
- 已经在你的任务上,对照 BF16 基线评估了所服务配置(量化、KV dtype、投机解码)的质量。
- 已为 TTFT/TPOT 百分位数、队列长度、KV 利用率、抢占次数和前缀缓存命中率建立了仪表盘和告警。
- 你清楚新副本的冷启动时间,并且至少留有足以覆盖这段时间的热备容量。
常见误区¶
- 只用一种 prompt 长度和一种输出长度做基准测试。 真实流量是有分布和长尾的;P99 由长尾决定。
- 只报告最大吞吐而不报告延迟。 它描述的是一个没人会部署的系统。要报告满足 SLO 的吞吐,或者 goodput。
- 只看服务端延迟。 用户体验到的是客户端 TTFT,包含排队、网络和 detokenization。
- 把闭环结果当成开环结果。 闭环基准测试在服务器变慢时会自己放慢到达速率,从而掩盖过载;线上流量更接近开环。
- 按 100% 利用率估算成本。 要除以真实的平均利用率。
- 假设不同 batch 大小或不同引擎下的输出相同。 没有 batch 不变的 kernel 时,同一个 prompt 可能因并发负载不同而产出不同的 token——这对评测、调试和 RL 都很重要。
本章要点¶
- 引擎要靠在你自己的模型、硬件和流量上做基准测试来选;vLLM 和 SGLang 覆盖了大多数数据中心的需求,TensorRT-LLM 面向 NVIDIA 上的极致性能,llama.cpp/MLC 面向本地与边缘。
- 用真实的负载形态做基准测试,扫描负载,并报告 TTFT/TPOT/ITL 百分位数、吞吐和 goodput。
- 容量 = 需求(token/s,以及通过 Little 定律得到的飞行中请求数)÷ SLO 下单副本的工作点,再加余量,并用 KV 显存核对。
- 每百万 token 成本 = (每小时 USD)÷(每小时百万 token 数);SLO 的严格程度和集群利用率与原始速度同样重要。
- 推理模型让解码和 KV 容量成为主导;RL rollout 则带来权重同步、异步以及数值一致性(batch 不变性)方面的要求。
自测¶
你的基准测试显示,并发 512 时有 10,000 output token/s,P99 TPOT 为 140 ms。你的 SLO 是 P99 TPOT ≤ 50 ms。做容量规划时该用哪个数字?
都不能直接用:在扫描结果中找到 P99 TPOT ≤ 50 ms 的最高并发,然后用在那一点测得的吞吐。10,000 token/s 那个点违反了 SLO。
λ = 5 请求/秒,平均端到端延迟 40 s。平均有多少请求在飞行中?
由 Little 定律,\(N = 5 \times 40 = 200\)。
一张 GPU 每小时花费 USD 3,在你的 SLO 下能持续输出 2,000 token/s。满负荷利用时,每百万输出 token 的成本是多少?50% 利用率时呢?
\(2000 \times 3600 = 7.2\) M token/小时 → \(3 / 7.2 \approx 0.42\) USD 每百万;50% 利用率时约 0.83 USD。
为什么一个 64 用户的闭环基准测试,相比同样平均速率下的开环流量,会低估过载?
在闭环中,每个用户要等到收到响应才发下一个请求,所以服务器变慢时,到达率会自动下降。开环的到达则不管怎样都会持续到来,所以一旦超过容量,队列(以及 TTFT)就会无限增长。
为什么同一个 prompt 用贪心采样,在服务器繁忙与空闲时可能给出不同的输出?
batch 的组成变了,kernel 可能选择不同的归约策略(split-K、split-KV、tile 大小);浮点数不满足结合律,使 logits 略有变化,argmax 中接近平局的情形就可能翻转,之后两次生成就会分叉。batch 不变的 kernel 可以避免这一点。
延伸阅读¶
| 论文 / 资源 | 为什么读 |
|---|---|
| vLLM | 事实上的开源标准引擎:PagedAttention、连续批处理、前缀缓存、分块预填充;V1 引擎。 |
| SGLang | RadixAttention、快速结构化输出、大规模 EP 与 PD 分离;在 RL rollout 中很流行。 |
| TensorRT-LLM | NVIDIA 优化的 kernel、Hopper/Blackwell 上的 FP8/FP4、in-flight batching。 |
| llama.cpp / Ollama | 面向 CPU/Apple Silicon/边缘设备的推理,带 GGUF 量化。 |
| MLC-LLM | 基于编译器的部署,目标包括手机、浏览器(WebGPU)和各类 GPU。 |
| Hugging Face TGI | 与 HF 生态集成的生产级服务器。 |
| Defeating Nondeterminism in LLM Inference (Thinking Machines) | 为什么推理不具备 batch 不变性,以及如何让它具备;对 RL 和可复现评测至关重要。 |
| verl | 把 vLLM/SGLang 嵌入为 rollout 引擎的 RL 后训练框架。 |
| slime | 围绕 SGLang rollout 和快速权重同步构建的 RL 框架。 |
| nano-vllm | 约 1,200 行、可读性强的引擎:用真实代码看懂第 3 部分全部内容的最佳方式。 |
| LLM Inference Handbook (BentoML) | 关于指标、基准测试和部署选择的实用指南。 |
| CMU 15-442/642 Machine Learning Systems | 涵盖 ML 系统方向(包括推理服务)的大学课程。 |
| GPU MODE lectures | 关于 GPU 编程和推理 kernel 的社区讲座。 |