CUDA Event:GPU 时间线上的事件、同步原理与实战

CUDA kernel launch 通常是异步的:CPU 把 kernel、内存拷贝等操作提交到 stream 后便继续执行。由此产生两个常见问题:如何准确测量 GPU 工作耗时,以及如何让不同 stream 在不阻塞 CPU 的情况下建立依赖? CUDA Event 就是解决这类问题的基础设施。它可以理解为插入 GPU stream 时间线中的一个标记:当 event 之前的工作全部完成,event 才进入完成状态。借助这个状态,我们可以做 GPU 计时、CPU 等待、跨 stream 同步和异步资源回收。 1. Event 不是 CPU 事件,而是 GPU 时间线标记 一个 CUDA stream 是按序执行的 GPU 工作队列: CPU 提交: Kernel A → memcpy → Event E → Kernel B 异步提交 GPU 执行: Kernel A ── memcpy ── E 完成 ── Kernel B 调用 cudaEventRecord(event, stream) 时,CPU 通常只是向 stream 提交一个 event 记录操作,并不会等待 GPU 执行到该位置。只有当这个 stream 中排在 event 前面的操作完成后,event 才会被标记为完成。 ...

2026年8月31日 · 6 分钟 · Hellokitty

SGLang 双 Stream 重叠调度:如何把 CPU 后处理藏到 GPU 计算背后

大模型自回归推理看起来是纯 GPU 工作:每一步做一次模型前向,采样一个 token,再把 token 喂给下一步。然而在高性能推理系统里,GPU 计算只是流水线的一部分。采样结果回传、EOS 判断、请求回收、KV Cache 管理、下一批组装和元数据准备,都可能在两次前向之间制造空洞。 SGLang 的 Overlap Scheduling 解决的正是这个问题。它不是简单地“多开一条 CUDA stream”,而是同时完成两件事: 让当前步产生的 token 留在 GPU,直接作为下一步输入,解除下一次 forward 对 CPU 读值的依赖; 用 scheduler stream 和 engine stream 构造软件流水线,让 GPU 计算当前批次时,调度器处理上一批次的结果。 本文以 Mini-SGLang 的 python/minisgl/scheduler/scheduler.py 为线索,解释 overlap_loop、_forward 和 _process_last_data 背后的依赖重排。 先看结论:优化的不是“提交速度”,而是依赖关系 CUDA kernel 的提交本来就是异步的。CPU 把工作放进 stream 后,不必等待 GPU 完成。因此一个自然的问题是:只用一条 stream,只要 CPU 提交得足够快,不也能让 GPU 连续工作吗? 如果系统只有计算任务,这个判断基本成立。但自回归推理存在一个控制依赖:调度器通常要知道采样出的 token,才能判断请求是否命中 EOS、是否应该释放资源,以及下一轮有哪些请求仍应留在 batch 中。 串行执行会形成如下链条: GPU: forward(B1) ── idle ── forward(B2) ── idle ── forward(B3) CPU: 处理 B1 处理 B2 读 token 读 token 判 EOS 判 EOS 组 B2 组 B3 GPU 完成 B1 后,CPU 才读取结果并决定 B2。此时 GPU 没有可执行的新工作,只能等待。这不是 CPU 调用 CUDA API 太慢,而是 B2 在逻辑上依赖 B1 的结果。 ...

2026年8月30日 · 4 分钟 · Hellokitty

SAC:面向稀疏注意力大模型的 CXL 解耦 KV Cache 系统

本文翻译并整理自论文 SAC: Disaggregated KV Cache System for Sparse Attention LLMs with CXL(arXiv:2606.19746v1,2026 年 6 月 18 日)。原论文采用 CC BY 4.0 许可。为适应博客阅读,本文省略参考文献列表中的完整出版信息,但保留正文结构、关键论证、实验结果与附录要点。 随着大模型进入长上下文与稀疏注意力时代,推理系统的主要瓶颈正从算力转向内存容量。传统 RDMA 解耦 KV Cache 系统会在解码前把完整前缀 KV Cache 搬回本地;但稀疏注意力每一步只访问少量 top-k 条目,这种“全量搬运、少量使用”的方式会同时浪费网络带宽和本地内存。 SAC 的核心思路是:使用 CXL 的低延迟、缓存行粒度 load/store 语义,把 KV Cache 留在解耦内存池中,只在计算时按需读取被选中的 top-k 条目。论文在 DeepSeek-V3.2 与 SGLang 上的实验显示,相比 RDMA 基线,SAC 可实现最高约 2.1 倍吞吐量、9.7 倍更低的 TTFT,以及 1.8 倍更低的 TBT。 摘要 大模型向长上下文推理扩展,使服务系统的主要瓶颈从计算能力转向内存容量。面向稠密注意力模型的传统方案通常采用基于 RDMA 的解耦内存池,在解码开始前,以粗粒度方式将整个前缀 KV Cache 从远端存储取回本地内存。 然而,这种方法并不适合新兴的稀疏注意力模型。解码过程中虽然只有很少一部分 KV 条目真正活跃,系统仍然要把完整 KV Cache 拉回本地,从而造成严重的传输瓶颈和本地内存浪费。 ...

2026年8月28日 · 5 分钟 · Hellokitty

Mooncake 中的协程:从 coro_rpc 到同步接口桥接

Mooncake 确实使用了协程,但它并不是把整个系统改造成“全异步架构”。截至本文分析的主分支提交 3d1665a,协程主要出现在控制面 RPC、连接管理和阻塞任务卸载等 I/O 密集路径;对外接口仍大量保留同步调用形式。 这形成了一个很实用的分层:内部用 C++20 协程组织异步流程,边界处再按需要转换成同步返回值或回调。 技术栈 Mooncake 的协程代码主要建立在两层库之上: async_simple::coro::Lazy<T> 表示一个惰性异步任务; yalantinglibs 的 coro_rpc 和 coro_io 提供异步 RPC、网络连接与线程池调度。 典型代码形态如下: async_simple::coro::Lazy<Result> request() { auto response = co_await client.send_request(...); co_return response; } Lazy<T> 创建后通常不会立刻执行。它需要被另一个协程 co_await,通过 .start(...) 启动,或者由 syncAwait(...) 驱动至完成。 路径一:Mooncake Store 的 Master RPC mooncake-store/src/master_client.cpp 中的 MasterClient::invoke_rpc 是最清晰的例子。它的外部签名是同步的: tl::expected<ReturnType, ErrorCode> MasterClient::invoke_rpc(Args&&... args); 函数内部却先构造 Lazy,再连续等待两个异步阶段: return async_simple::coro::syncAwait( [&]() -> async_simple::coro::Lazy< tl::expected<ReturnType, ErrorCode>> { auto pending = co_await pool->send_request( [&](coro_io::client_reuse_hint, coro_rpc::coro_rpc_client& client) { return client.send_request<ServiceMethod>(...); }); if (!pending.has_value()) { co_return tl::make_unexpected(ErrorCode::RPC_FAIL); } auto result = co_await std::move(pending.value()); co_return result->result(); }()); 这里有两次 co_await: ...

2026年8月28日 · 2 分钟 · Hellokitty

tiny-llm:在 Apple Silicon 上从 Transformer 算子走到推理系统与 Agent

如果你已经知道 Transformer 的基本结构,却仍然不清楚 vLLM 一类推理系统为什么需要 KV Cache、Continuous Batching、Paged Attention,以及这些技术最终如何支撑 Agent,skyzh/tiny-llm 是一条很好的动手路径。 它不是一个追求生产可用的迷你框架,而是一门以 Qwen3 和 MLX 为载体的系统课程:先用可读的数组运算实现模型,再围绕真实性能瓶颈写 Metal 内核,随后搭出动态批处理和分页缓存,最后把同一个本地模型接入带工具、检查点、压缩、评测和分支选择的 Agent 循环。 截至 2026 年 8 月 27 日,项目约有 4.5k stars,使用 Apache-2.0 许可证。课程主体分为四周,已覆盖从注意力到受控 Agent 的完整主线。 项目定位:不是“再写一个 Transformer” 很多教学项目止步于“加载权重并生成一句话”。tiny-llm 的不同之处,是把模型实现当作起点,而不是终点。 它试图回答三个递进的问题: 一个现代开源模型如何由矩阵运算变成文本? 一个正确但缓慢的实现,如何通过测量和内核优化接近成熟框架? 一个推理引擎如何进一步成为可暂停、可审计、可恢复的 Agent 执行器? 项目选择 Apple Silicon、MLX 和 Qwen3。统一内存让 CPU、GPU 和模型权重处在相对简单的硬件环境里;MLX 提供熟悉的 Python 数组接口,同时允许下沉到 Metal;Qwen3 则带有 GQA、QK Norm、BF16 激活和 4-bit 权重,足以暴露真实推理系统中的带宽、注意力与缓存问题。 代码结构刻意分成两套: src/tiny_llm/ 是学习者逐步补全的实现; src/tiny_llm_ref/ 是参考实现,用于测试和基准对照; src/extensions/ 与 src/extensions_ref/ 分别放置 Metal/C++ 扩展; tests_refsol/ 按周和天组织验收测试; book/ 是完整课程正文。 这种布局把“阅读—实现—测试—测量”闭成了环。 ...

2026年8月27日 · 3 分钟 · Hellokitty

从朴素循环到硬件极限:GEMM / GEMV 高性能算子优化指南

GEMM 与 GEMV 看起来都只是乘加: GEMM: C = alpha * A * B + beta * C GEMV: y = alpha * A * x + beta * y 但二者的最佳实现完全不同。GEMM 可以反复复用矩阵块,通常有机会逼近计算峰值;GEMV 中矩阵元素通常只读一次,往往受内存带宽限制。高性能算子的第一步不是写 SIMD 或 CUDA,而是先判断瓶颈究竟在哪里。 本文给出一条从正确基线走向高性能内核的完整路线。重点不是某段固定代码,而是每一步为什么有效、如何验证,以及何时应该停止优化。 一、先建立性能上限 1. 计算量 对于矩阵尺寸: A: M × K B: K × N C: M × N GEMM 约执行: FLOPs = 2 * M * N * K GEMV 是 N = 1 的特殊形态: FLOPs = 2 * M * K 乘法和加法各算一次浮点操作。 ...

2026年8月26日 · 6 分钟 · Hellokitty

NVIDIA Dynamo 源码阅读(二):分布式运行时如何组织服务与请求

Dynamo 的推理组件能够跨进程、跨节点组合,核心依赖 lib/runtime。这一层不理解模型,也不关心 token 的语义;它提供的是一套分布式组件模型:服务如何命名、实例如何注册、客户端如何发现、请求如何流式传输。 本文基于 Dynamo 提交 27f09d5。 一、运行时的核心抽象 源码从 DistributedRuntime 开始。它持有运行时配置、服务发现实现、网络管理器和关闭信号,并向上提供 Namespace。 对象关系可以简化为: DistributedRuntime └── Namespace("dynamo") ├── Component("frontend") │ └── Endpoint("generate") ├── Component("router") │ └── Endpoint("generate") └── Component("worker") └── Endpoint("generate") × N instances 这四层各有不同职责: Runtime:进程级资源和生命周期; Namespace:隔离一组服务; Component:表示逻辑服务角色; Endpoint:表示可调用接口及其多个实例。 Endpoint 名字相同不代表只有一个服务器。每个 Worker 可注册自己的 instance,客户端通过 discovery 得到动态实例集合。 二、服务发现保存什么 lib/runtime/src/discovery 定义了发现层。一个 Endpoint 注册时,系统不仅记录地址,还附带实例、组件和元数据。客户端查询的关键维度是: (namespace, component, endpoint) -> [instance...] 在裸机模式中,注册与租约可由 etcd 支持;Kubernetes 模式使用 DynamoWorkerMetadata CRD 和 EndpointSlice。运行时把发现后端抽象掉,上层 Router 不需要分别实现 etcd watcher 与 Kubernetes watcher。 ...

2026年8月26日 · 2 分钟 · Hellokitty

NVIDIA Dynamo 源码阅读(三):KV-aware Router 如何选择 Worker

普通负载均衡只关心哪个 Worker 更空闲。LLM 推理还需要回答另一个问题:哪个 Worker 已经缓存了当前请求的最长前缀?如果忽略缓存局部性,相同 system prompt、文档或多轮会话会在不同 GPU 上反复 Prefill。 Dynamo 的 KV-aware Router 将缓存命中与运行负载放进同一次决策。本文基于提交 27f09d5,重点阅读 lib/kv-router 与 lib/llm/src/kv_router。 一、路由问题如何形式化 设请求 token 被按固定 block size 切成若干块: [t0 ... t15] [t16 ... t31] [t32 ... t47] [剩余 token] B0 B1 B2 每个完整块计算链式哈希。Router 维护如下关系: B0 -> {worker 1, worker 3} B1 -> {worker 1} B2 -> {worker 1} 如果新请求具有相同前三块,Worker 1 的重叠长度最大。但它可能已经非常繁忙,因此最终决策不是简单的最长前缀匹配,而是 locality 与 load 的权衡。 二、为什么使用链式 block hash compute_block_hash_for_seq 不会孤立地哈希每个 token 块,而是把前一个块的 hash 纳入下一个块。这使相同内容出现在不同前缀后时不会被错误视为同一缓存位置。 ...

2026年8月26日 · 2 分钟 · Hellokitty

NVIDIA Dynamo 源码阅读(四):Prefill/Decode 分离与 KV 传输

Prefill 与 Decode 使用同一个 Transformer,却具有不同资源特征。Prefill 面向长序列并行计算,关注首 token 延迟;Decode 每轮只处理少量 token,受显存带宽和迭代调度影响。把两者固定绑在同一副本上,资源比例只能随实例一起扩缩。 Dynamo 的 disaggregated serving 将两阶段放到不同 Worker 池,并用直接 KV 传输连接起来。本文基于提交 27f09d5。 一、为什么要做两次路由 解耦请求包含两个独立选择: Frontend | v 选择 Prefill Worker ----> 计算输入 KV | | |<---- transfer metadata--+ | v 选择 Decode Worker <==== 直接搬运 KV | v 逐 token 生成 -> Frontend -> Client Prefill Worker 的选择偏向输入前缀命中与 Prefill 排队;Decode Worker 的选择偏向持续生成负载、可用 KV 容量和传输拓扑。将二者合成一次普通负载均衡,会丢失阶段差异。 ...

2026年8月26日 · 2 分钟 · Hellokitty

NVIDIA Dynamo 源码阅读(一):数据中心级推理栈的整体架构

NVIDIA Dynamo 不是另一个 vLLM 或 TensorRT-LLM。它不负责重新实现 Attention、Continuous Batching 或模型执行,而是把多个推理引擎实例组织成一个可路由、可拆分、可扩缩的服务。 这一区分决定了阅读源码的入口:不要从 CUDA kernel 开始,而应先找到请求如何跨过 Frontend、Router、Prefill Worker 和 Decode Worker,以及服务发现、事件传播、KV 传输分别位于哪一层。 本文基于 Dynamo 提交 27f09d5,仓库版本为 1.5.0。 一、Dynamo 解决的不是单卡执行问题 单个推理引擎已经能完成一次生成:接收 token、执行 Prefill、循环 Decode、管理本机 KV Cache。规模扩大后,系统出现新的问题: 同一前缀可能被多个副本重复 Prefill。 Prefill 与 Decode 的算力和延迟特征不同,固定配比容易浪费 GPU。 Worker 上下线后,路由器必须快速获得最新拓扑。 KV Cache 不仅存在于显存,还可能分布在主存、SSD 或其他节点。 不同后端有不同请求格式与 KV 传输协议,但上层服务不应被绑定。 Dynamo 把这些问题放在推理引擎之上: 控制面 Discovery / Planner / Metrics | v Client -> Frontend -> Router -> Inference Worker | | | | +---- KV Events+ | +------ 流式响应 ----------> KV 数据面(按需) Prefill Worker ======> Decode Worker NIXL / backend connector 请求平面回答“请求发给谁”;KV 数据面回答“缓存如何移动”;控制面回答“哪些实例存在、负载怎样、是否扩缩容”。三者分开,是 Dynamo 架构最重要的设计。 ...

2026年8月26日 · 2 分钟 · Hellokitty