第 13 章 · LLM 必备零件

⏱️ 70 分钟🎯 凑齐 Capstone 所需算子📂 code/ch13_llm_parts/

本章拼齐第 14 章 Capstone 还差的算子。每个独立、简单,但少一个就跑不起来一个完整 LLM。

学习目标

前置知识

需要掌握第 7 章的 reduction/scan、第 9 章的 GEMM,以及第 10–12 章的 softmax 与 attention 数据流。

核心概念

本章关注 LLM 推理中 GEMM 和 attention 之外的必要部件,以及它们对显存容量、带宽和生成质量的影响。

关键代码

13.1 RoPE — Rotary Position Embedding

Llama / Mistral / Qwen 都用 RoPE 替代了 GPT-2 的绝对位置编码。 它把 Q、K 向量看成 D/2 个复数,第 i 对复数乘以 exp(j · θ_{t,i}),其中 θ_{t,i} = t / base^{2i/D}

实现(in-place)

__global__ void rope_inplace(float* x, int T, int D, float base) {
    int t = blockIdx.x;
    int i = threadIdx.x;
    int half = D / 2;
    float theta = float(t) / powf(base, float(2*i) / float(D));
    float c = cosf(theta), s = sinf(theta);
    float x0 = x[t*D + i];
    float x1 = x[t*D + i + half];
    x[t*D + i       ] = x0 * c - x1 * s;
    x[t*D + i + half] = x0 * s + x1 * c;
}

性质:

13.2 SwiGLU / SiLU — Llama FFN

GPT-2 用 FFN(x) = GELU(x @ W_1) @ W_2。 Llama 用双投影 + 门控的 SwiGLU:

FFN(x) = ( SiLU(x @ W_gate) ⊙ (x @ W_up) ) @ W_down

SiLU 定义:silu(x) = x · sigmoid(x) = x / (1 + e^{-x})

__device__ float silu(float x) {
    return x * (1.f / (1.f + __expf(-x)));
}

__global__ void swiglu(const float* G, const float* U, float* O, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) O[i] = silu(G[i]) * U[i];
}

"门控"让网络学会"对某些位置让信息通过、对另一些抑制"。代价:FFN 多一个 W_gate,参数量 ~1.5×;但 hidden = 2.66·D(GPT-2 是 4·D),总参数差不多。

13.3 KV Cache

自回归生成有个关键观察:第 t 步生成的 token i 时,第 t-1 步算出来的 K、V 仍然有效,不必重算。 于是推理代码维护一个 cache:

K_cache shape: (n_layers, n_heads, T_max, D_head)
V_cache shape: same

每步只算新 token 的 (K_new, V_new) 形状 (1, D_head),append 到 t_pos 位置:

__global__ void append_kv(const float* K_new, const float* V_new,
                          float* K_cache, float* V_cache, int t_pos, int D) {
    int d = blockIdx.x * blockDim.x + threadIdx.x;
    if (d < D) {
        K_cache[t_pos * D + d] = K_new[d];
        V_cache[t_pos * D + d] = V_new[d];
    }
}

KV cache 的显存开销

Llama-7B (n_layers=32, n_heads=32, D_head=128, fp16) 一个 token 的 KV 占 32 × 32 × 128 × 2 × 2 B = 0.5 MB。 T=2048 时 1 GB;batch=8、T=2048 → 8 GB。这就是为什么 KV cache 管理(PagedAttention 等)这么重要。

13.4 Sampling 策略

greedy (argmax)

// 简化: V <= 65536 用单 block reduce 即可
template <int BLOCK>
__global__ void greedy_argmax(const float* logits, int* out, int V) {
    __shared__ float vals[BLOCK]; __shared__ int idxs[BLOCK];
    float bv = -INF; int bi = -1;
    for (int i = tid; i < V; i += BLOCK)
        if (logits[i] > bv) { bv = logits[i]; bi = i; }
    /* block-wide reduce on (vals, idxs) keeping max */
    if (tid == 0) *out = idxs[0];
}

top-k (truncated sampling)

从 logits 取最大的 k 个,softmax,按概率抽样。常见 k = 40~50。GPU 实现:用 block-wide top-k(heap 或 partial sort)。

top-p (nucleus sampling)

排序 logits → 累计概率 → 截断到累积 ≥ p 的前缀(典型 p = 0.9~0.95)→ 重新归一 → 抽样。需要 sort + prefix scan,第 7 章的 scan 模板在这里能复用。

temperature

logits /= T,T > 1 让分布更平(更随机),T < 1 让分布更尖(更确定)。在 softmax 前应用。

策略速度多样性用途
greedy最快最低评测、确定性回放
top-k中等中等chat, 一般生成
top-p稍慢较高chat, 创意写作
temperature only可调组合用

运行结果

在目标 NVIDIA GPU 上分别运行本章示例;每个程序应打印 CPU/GPU 对拍结果,性能数字保留为本机实测值,不跨 GPU 照搬。

自检清单

Q1: RoPE 为什么 in-place 还能正确?

因为旋转是 pair-wise (x0, x_{D/2}) ↔ (x0', x_{D/2}'),写入新位置时旧的两个值都还在寄存器里(在 kernel 内已加载),所以 in-place 安全。

Q2: KV cache 为什么放 GPU 显存而不是 CPU?

attention 算 (Q_new @ K_cache^T) 要把 K_cache 全部读进 SM。放 CPU 每步都得 H2D,PCIe / C2C 路径通常比本地 HBM 慢且延迟更高。所以 KV cache 是 LLM 显存大户。

Q3: SwiGLU 比 GELU 强多少?

不算"强很多"。Llama 团队论文报告 +0.3 PPL 左右改善。但因为没坏处+实现简单,新模型都默认用它。

Q4: top-p 排序很贵吗?

大词表完整排序会增加采样开销。优化套路:先 top-k 截断再排序,或者使用 radix-select;请在目标引擎、词表与采样参数上记录延迟。

Q5: prefill 和 decode 在 KV cache 上有什么区别?

prefill(处理 prompt):一次性算 T_prompt 个 KV 写 cache,是大 batch GEMM;decode:每步只算 1 个 KV,是 GEMV(memory-bound)。所以 LLM 服务把两者分开调度。

练习题

  1. 实现 top-k sampling:先 block-wide top-k(用堆),再 softmax 抽样。
  2. 实现 top-p (nucleus):sort + scan + 截断。
  3. rope.cutheta_base = 500000(Llama 3 用),看长 T 时角度变化。
  4. swiglu 改成 fused:把 silu(G) * U 合并到 W_down 的 GEMM 里(pre-multiplier trick)。

13.7 工业实战:量化、投机解码、采样工程

13.7.1 W4A16 量化 — LLM 推理头号加速武器

LLM decode 阶段瓶颈是权重的 HBM 读带宽(M=1 时 FLOPs 极少)。 最低延迟可以先用 weight_bytes / measured_HBM_bandwidth 估算。 把权重从 fp16 压成 INT4 会减少编码后的权重字节;端到端结果还要计入 dequant、scale 读取、cache reuse 和 batch 复用。这就是 W4A16(W=int4,A=fp16)。

算法思路精度损失实现
GPTQ逐层 OBS 找量化误差最小的舍入< 0.5 PPLAutoGPTQ
AWQactivation-aware: 保护"重要"通道< 0.3 PPLllm-awq, vLLM
GGUFblockwise scale, k-means0.2-0.8 PPLllama.cpp
SmoothQuant同时量化 W+A (W8A8)~0.2 PPLTensorRT-LLM

W4A16 kernel 的关键:dequant + GEMM 融合

朴素做法:先把 INT4 dequant 成 fp16 写回 HBM 再调常规 GEMM —— 这样消除不了带宽瓶颈
正确做法:kernel 内即时 dequant,权重始终以 INT4 从 HBM 读:

for (int kt = 0; kt < K; kt += BK) {
    // 1) HBM 只读 INT4 weight tile (BM*BK/2 字节, 2 个 INT4 / byte)
    load_int4_to_shared(W_int4_tile, gmem_ptr);
    // 2) shared 内 dequant: int4 -> fp16, 乘 per-group scale
    dequant_inplace(W_int4_tile, W_fp16_tile, scales[group_id]);
    // 3) WMMA / mma.sync 用 fp16
    wmma::mma_sync(acc, A_frag, B_frag, acc);
}

典型实现:TensorRT-LLM weight_only_gemm,vLLM 的 Marlin kernel。实际 tokens/s 与 GPU、batch、上下文长度、group size 和调度器强相关;本教程不写死未经本仓 benchmark 验证的吞吐数字。

13.7.2 投机解码 (Speculative Decoding)

用小模型快速生成 N 个候选 token,大模型一次性验证。接受多少多少出,拒绝就退回。

graph LR
    Start["prompt"] --> Draft["draft model
(TinyLlama 1B 等)
快速 4-8 token"] Draft --> Verify["target model
(Llama 70B)
一次前向算 5 个位置 logits"] Verify --> Accept{"p_target / p_draft
是否接受?"} Accept -->|"接受"| Continue["输出, 继续 draft"] Accept -->|"拒绝"| Resample["从拒绝位重采样, 退回 draft"] Continue --> Draft Resample --> Draft style Draft fill:#f3f1e8,stroke:#8b1538 style Verify fill:#f3f1e8,stroke:#2f5d3a

关键洞察:

draft model 候选:

vLLM、TensorRT-LLM 都内置。EAGLE / Medusa 需要额外训练,n-gram 0 训练成本。

13.7.3 生产 sampler — 不只是 argmax

// 完整 pipeline: logits -> /temp -> top-k -> top-p -> softmax -> cuRAND 抽样
__global__ void sample_kernel(float* logits, int V, float temp, int top_k, float top_p,
                              unsigned int seed, int* out_token) {
    // 1) 除 temperature
    for (int i = tid; i < V; i += BLOCK) logits[i] /= temp;
    // 2) top-k: 保留最大 k 个, 其他 -inf  (block-wide heap 或 partial sort)
    block_topk(logits, V, top_k);
    // 3) softmax + inclusive scan + 找 top-p 截断点 + 重新归一
    block_softmax(logits, V);
    block_inclusive_scan(logits, V);
    int cutoff = block_find_first_ge(logits, top_p);
    // 4) 用 cuRAND 抽样
    curandState s; curand_init(seed, tid, 0, &s);
    *out_token = sample_from_dist(logits, cutoff, curand_uniform(&s));
}

性能:请用第 15 章的 nsys/ncu 流程在目标 GPU 上采集。记录项包括 vocab size、top-k/top-p、batch、sampler kernel latency 与 decode 总耗时占比。

13.7.4 KV cache 量化(int8 / fp8)

权重量化外,KV cache 也可以量化。编码字节减少后可能容纳更大 batch 或更长 context,但需计入 scale、metadata 与 allocator 对齐。

实现:per-group scale,attention kernel 内 load 时 dequant:

// attn 读 K 时:
int8_t k_i8 = K_cache_int8[t * D + d];
half scale  = K_scales[t / GROUP_SIZE];          // 每 64 token 一个 scale
half k      = __int2half_rn(k_i8) * scale;

TensorRT-LLM 与 vLLM V1 都提供量化 KV cache 路径,但支持矩阵、scale 粒度和质量约束必须按当前文档核验。 陷阱:per-tensor、per-channel、per-group 各有精度、元数据和调度成本,不能把固定 group_size 当作所有模型的生产默认值。

13.7.5 RoPE 工程注意点

13.7.6 推理优化优先级

优化prefill 收益decode 收益实施难度
fp16 → fp8 weight★★★★★★
W4A16 量化★★★★★
FlashAttention v2★★★★★★★★易(用现成库)
PagedAttention★★★★(吞吐)中(vLLM)
Speculative decoding★★★★
CUDA Graph★★
Continuous batching★★★★(吞吐)
fused norm + QKV★★

顺序建议:先用 vLLM/TRT-LLM 这类成熟引擎建立可用 baseline,再针对自己模型做 fp8 量化 / spec decoding 增量调优,并用同一套 trace 比较。

13.8 研究前沿(2025-2026):EAGLE-3、Lookahead、KV 压缩、W4A4 / FP4

13.8.1 投机解码 2024-2026 演进

方法原理复现指标实现成本
Vanilla SpecDec (2023)独立小 draft modelaccept rate、draft cost、verify batch需训练 draft
Medusa(2024)大模型加多个并行 head 直接预测后 N tokenhead accuracy、树宽、verify cost微调 + head 训练
EAGLE(ICML 2024)用大模型 hidden state 作为 draft head 输入accept length、TPOT、batch 稳定性训练 draft head
EAGLE-2(2024)动态接受树, 不是固定 chain动态树分布、graph bucket 命中同 EAGLE
EAGLE-3(2025)更深 draft head, 多层 hidden 输入draft/verify 时间比同 EAGLE
Lookahead Decoding(Hao Zhang 团队 2024)用 Jacobi 迭代 + n-gram cache, 无需训练n-gram 命中、请求分布0 训练
Hydra(2024)多 head 串行依赖, 比 Medusa 接受率高候选树接受率训练
ReDrafter(Apple 2024)RNN draft + Top-K beam, 上线 LLM Enginebeam 宽度、额外状态成本训练

2026 工业现实

13.8.2 KV cache 压缩前沿(2024-2026)

除了 13.7.4 的量化路线,2024-2026 出现了大量"丢 token"型压缩:

方法核心想法节省来源复现指标
StreamingLLM(ICLR 2024)保留 sink token + 最近 W token保留 token 数下降长上下文任务准确率 / PPL
H2O (Heavy-Hitter Oracle)累计 attention score 高的 token 留稀疏保留历史 KVattention score 统计成本 / PPL
SnapKV(NeurIPS 2024)每层独立选 K/V 保留层级选择性保留每层保留率 / 召回质量
KIVI(ICML 2024)K per-channel 量化, V per-tokenKV dtype 降字节量化误差 / PPL / kernel 成本
KV-Quant非均匀量化 + outlier 单独保留低 bit + outlier 路径outlier 比例 / dequant 成本
Quest(ICLR 2025)查询时 page-level 选 top-k 块取决于 sparsity近无损
L2Compress / MiniCache跨层 KV 合并跨层冗余下降层间共享策略 / PPL

组合:MLA + fp8 KV + SnapKV / Quest 是 DeepSeek-V3 / Kimi 等方向的工业实践。长 context 的 KV 显存节省要按模型层数、head_dim、保留 token 数和精度策略逐项测算。

13.8.3 W4A4 / NVFP4 — 极致量化(2024-2026)

9.10.4 给了对比表,这里详述算法:

QuaRot(ICML 2024)— Hadamard 旋转解 outlier

激活的 outlier 是 W4A4 最大障碍(少数 channel 数值范围比平均大 100×)。QuaRot 用 Hadamard 矩阵 H 旋转:x' = xH, w' = wH^T数学等价 + 旋转后分布更均匀,outlier 被打散

SpinQuant(Meta 2024)— 可学习旋转

不用固定 Hadamard,训练学习旋转矩阵。精度比 QuaRot 再好 0.2-0.5 PPL。

Atom(MLSys 2024)— 混合 W4A4 + Heuristic outlier

正常 channel 用 INT4,outlier channel 单独留 INT8。fp16 baseline 之上无损。

NVFP4(Blackwell 2025)— 硬件原生

fp4 (E2M1) + per-16-block fp8 scale,Tensor Core 直接吞。无需 dequant 到 fp16/fp8 中转,吞吐拉满。是 2025+ B200 推理的事实标准

13.8.4 BitDelta / 1-bit weight diff

BitDelta(Anthropic 2024):把 fine-tuned 模型相对 base 的 diff 量化到 1 bit

意义:多租户服务能存几百个 fine-tune 版本同时运行,每个只多花一点显存。Anthropic / Together AI / Fireworks 都在用。

13.8.5 RoPE 长 context 外推:YaRN、PI、NTK

原始 RoPE base=10000 训练长度内表现好,超出训练长度急剧退化。2023-2025 主要技术:

技术核心典型外推
Position Interpolation (PI)position 缩放
NTK-aware高频维度少缩、低频维度多缩
YaRN(ICLR 2024)NTK-aware + 温度调整 + 部分维度不缩16-32×
Dynamic NTK推理时根据当前 T 动态调 base10×+
LongRoPE(Microsoft 2024)进化算法搜索 per-dim 缩放因子32× (2M token)

Llama 3 直接训练到 8K,推理用 YaRN 外推到 128K。Llama 4 据传训练到 32K,外推到 10M。不重训能让模型多用 10×+ context,性价比极高。

13.8.6 Reasoning 模型的采样新需求

o1 / R1 类 reasoning 模型,采样策略影响最终能力:

对 sampler kernel 的影响:不再是单 next-token argmax,而是 N 个并行候选 + reward model 评估 + 选择,采样跟 attention 计算量同阶

13.8.7 2026 LLM 算子优化总览

技术主要改善项
FA v3 + Hopperprefill attention 的 Tensor pipe / TMA pipeline
NVFP4 + Blackwell低精度 GEMM、权重/激活字节和 scale 管理
MLA(如果模型用)KV cache 结构性压缩,提升可承载 batch/context
EAGLE-3decode 接受长度与 verify batch 效率
Lookahead decoding无需训练的 n-gram / Jacobi 候选复用
PagedAttention + RadixAttentionKV pool 利用率与前缀复用
Chunked prefillTTFT、队列公平性和 prefill/decode 混排
Disaggregated servingprefill/decode 资源分离后的集群利用率

不要把单项收益直接相乘:很多技术优化的是同一段瓶颈,叠加后会互相稀释。工业评估应以固定模型、固定 SLO、固定硬件的 tokens/$、TTFT、TPOT 和 p99 为准。

常见坑

13.10 CUDA 官方手册精讲(CUDA Programming Guide 13.2(核验:2026-07-20))

__sincosf / __half2 / KV layout / cuRAND / cudaMallocAsync

本节定位:把 NVIDIA 官方 CUDA Programming Guide 13.2(核验:2026-07-20) 当前版中和本章直接相关的硬核细节抽出来——概念、API、踩坑点、版本兼容性—— 让你不必通读官方手册也能掌握本章主题的"标准答案"。引用按命名章节回查,避免把旧版编号当作稳定接口。

RoPE 工程提速:__sincosf 取代 sinf/cosf

原版 cosf / sinf 走的是 CUDA 数学库的 IEEE 精度路径,单次调用展开为 ~20+ SASS 指令;而 device intrinsic __sincosf(x, &s, &c) 一次调用同时算出 sin/cos,只用 SFU(Special Function Unit)路径。 对 RoPE 这种每元素一次三角函数的算子,通常值得替换后用 SASS 和 benchmark 复核。

CUDA Programming Guide §5.5.9.2 给出的 ULP 误差:

函数最大 ULP 误差验证项
__sinf(x)2-21.41 abs(x ∈ [-π, π])SASS 指令数 + TODO(on GPU)
__cosf(x)同上SASS 指令数 + TODO(on GPU)
__sincosf(x, sp, cp)同上, 但一次调用SASS 指令数 + TODO(on GPU)
__expf(x)2 + ⌊|1.173·x|⌋SASS 指令数 + TODO(on GPU)
__fdividef(x, y)2 ULP(|y| ∈ [2-126, 2126])SASS 指令数 + TODO(on GPU)

改写后的 RoPE:

__global__ void rope_inplace_fast(float* x, int T, int D, float base) {
        int t = blockIdx.x;
        int i = threadIdx.x;
        int half = D / 2;
        // __powf uses the fast-math path; verify accuracy before replacing powf.
        float theta = float(t) * __powf(base, -float(2*i) / float(D));
        float s, c;
        __sincosf(theta, &s, &c);          // 一次 SFU 调用
        float x0 = x[t*D + i];
        float x1 = x[t*D + i + half];
        x[t*D + i       ] = x0 * c - x1 * s;
        x[t*D + i + half] = x0 * s + x1 * c;
    }

注意点:

SwiGLU 用 __half2 打包加速 (2 元素 / 指令)

Llama / Qwen 等模型 FFN 走 fp16 / bf16 路径。Hopper 之前的硬件没有原生 fp16-vector ALU,但 __half2(CUDA Math API 提供)把两个 fp16 打包到 32-bit 寄存器, 一条 PTX 指令一次算两个元素:HBM 读带宽利用率 +100%、寄存器压力 -50%。

#include <cuda_fp16.h>

    __device__ __forceinline__ __half2 silu2(__half2 v) {
        // SiLU(x) = x / (1 + exp(-x))
        // hexp2 + h2div + h2add  (HW 加速; sm_53+)
        __half2 one  = __float2half2_rn(1.0f);
        __half2 neg  = __hneg2(v);
        __half2 e    = h2exp(neg);                  // 包含 __hexp on each lane
        __half2 den  = __hadd2(one, e);
        return __h2div(v, den);
    }

    __global__ void swiglu_fp16(const __half2* G, const __half2* U,
                                __half2* O, int n_pairs) {
        int i = blockIdx.x * blockDim.x + threadIdx.x;
        if (i >= n_pairs) return;
        __half2 g = G[i], u = U[i];
        O[i] = __hmul2(silu2(g), u);                // 一条 HMMA-style mul, 2 元素
    }

调用端 launch 时 n_pairs = N / 2,原始 N 个 fp16 元素以 __half2* 重新解释即可。

更激进的写法(compute capability 8.x+):

陷阱:

KV Cache 三种内存布局 (BSHD / BHSD / Paged) 对比

同样的 4-D 张量 (Batch, Heads, Seq, Dim),存储 stride 不同会导致 attention kernel 的访存模式完全不一样。三个主流 layout:

LayoutStride (低 → 高)谁在用优势劣势
BSHDD, H, S, BHuggingFace Transformersappend 新 token 简单(沿 S 维 contiguous)每 head 跨 S 维访存不连续
BHSDD, S, H, BFasterTransformer, FlashAttentionattention QKT 沿 S 维 coalescedappend 时跨 head 写散
Paged页内 BHSD, 页表跳转vLLM (PagedAttention)变长序列零碎片kernel 一层间接寻址

(1) 朴素 BHSD append

// K_cache shape (B, H, T_max, D), row-major BHSD
    // append 第 t_pos 步的 K_new shape (B, H, D)
    __global__ void append_kv_bhsd(const __half* K_new, __half* K_cache,
                                    int B, int H, int T_max, int D, int t_pos) {
        int b = blockIdx.z;
        int h = blockIdx.y;
        int d = blockIdx.x * blockDim.x + threadIdx.x;
        if (d >= D) return;
        int dst = ((b * H + h) * T_max + t_pos) * D + d;
        int src = (b * H + h) * D + d;
        K_cache[dst] = K_new[src];
    }

K_cache 显存预分配 B*H*T_max*D*sizeof(half)。如果 batch 中真实序列长度差异 大(比如 chat 场景的 prompt 长度跨度很大),按 T_max 为每个请求预分配会造成随 trace 变化的显存浪费。 这就是 vLLM 提出 PagedAttention 的动机。

(2) Paged 布局 — 仿 OS 虚拟内存

// K_pool: (n_pages, page_size, H, D), 全局物理页池
    // block_table: (n_req, max_pages_per_req)  逻辑页 -> 物理页
    __device__ __half* k_at(int req_id, int t_pos,
                             const __half* K_pool, const int* block_table,
                             int page_size, int H, int D) {
        int logical_page = t_pos / page_size;
        int offset_in_page = t_pos % page_size;
        int phys_page = block_table[req_id * MAX_PAGES + logical_page];
        return (__half*)K_pool + (((size_t)phys_page * page_size + offset_in_page) * H * D);
    }

典型 page_size = 16 token(vLLM 默认)。优势:

代价:attention kernel 每访问一个 (b, t, h, d) 元素要先查 block_table,多一层间接寻址; FlashAttention v2.4+ 的 paged kernel 把 block_table 缓存到 shared memory 摊平开销。

采样工程:cuRAND Philox + warp-level reduce

生产 sampler 必须满足三个性质:(1) 可复现(同 seed 同输出),(2) 一次启动多请求并行采样不串扰, (3) latency 需要足够低,不能盖过 sampler 本身。cuRAND 的 curandStatePhilox4_32_10_t 是常用选择: 无状态、counter-based、单线程一次产 4 个 uniform 数,特别适合 LLM 大 batch 解码。

#include <curand_kernel.h>

    // 一个请求一个 state, 跨步骤增 counter
    __device__ float draw_uniform(curandStatePhilox4_32_10_t* st) {
        return curand_uniform(st);   // 单次调用 1 个 [0, 1) float
    }

    __global__ void sample_topp_kernel(const float* sorted_probs,   // 已 softmax 排过序
                                        const int*   sorted_idx,
                                        int V, float top_p,
                                        uint64_t seed, uint64_t step,
                                        int req_id, int* out_token) {
        extern __shared__ float scan[];

        // 1) 初始化此请求此步骤的 Philox state
        curandStatePhilox4_32_10_t state;
        curand_init(/*seed=*/seed, /*subseq=*/req_id, /*offset=*/step, &state);

        // 2) block-wide inclusive scan 找 top-p 截断点 (Ch7 模板)
        //    cum_p[V] 累计概率
        block_inclusive_scan(sorted_probs, scan, V);

        // 3) lane 0 取一个 [0, 1) 样本, broadcast
        float r = 0.f;
        if (threadIdx.x == 0) r = curand_uniform(&state);
        r = __shfl_sync(0xFFFFFFFFu, r, 0);

        // 4) 二分找 r 落在 scan[] 哪个 bucket; 注意截断到 cum_p <= top_p
        //    简化版: 每线程 stride 扫一遍, atomicMin 记录命中 index
        __shared__ int hit;
        if (threadIdx.x == 0) hit = V - 1;
        __syncthreads();
        float r_scaled = r * min(scan[V - 1], top_p);
        for (int i = threadIdx.x; i < V; i += blockDim.x) {
            if (scan[i] >= r_scaled) atomicMin(&hit, i);
        }
        __syncthreads();
        if (threadIdx.x == 0) *out_token = sorted_idx[hit];
    }

三个关键工程点:

// Hopper / Ampere 上的 fast argmax
    __device__ int warp_argmax(float val, int idx) {
        // 把 (val, idx) 编码到 uint64 高低位
        uint64_t pack = (uint64_t(__float_as_uint(val)) << 32) | unsigned(idx);
        // 注意: __reduce_max_sync 仅支持 unsigned/int, 这里走手写 shuffle
        for (int off = 16; off > 0; off /= 2) {
            uint64_t other = __shfl_xor_sync(0xFFFFFFFFu, pack, off);
            if (other > pack) pack = other;
        }
        return int(pack & 0xFFFFFFFFu);
    }

性能数据采集表(V=50257, 单请求;需在目标 GPU 上实测):

实现latency备注
朴素 thrust::sort + scanTODO(on GPU)记录是否有 per-call malloc
radix top-256 + block scanTODO(on GPU)记录 top-k/top-p 参数
radix top-256 + warp scan + PhiloxTODO(on GPU)记录随机数 state 初始化方式
fused with last logit GEMVTODO(on GPU)记录是否减少一次 HBM 读写

cudaMallocAsync — LLM 推理的零开销 allocator

朴素 LLM 服务在请求到来时 cudaMalloc 一块 KV cache,请求结束 cudaFree。 这俩调用是同步、跨进程串行化的,在高 QPS 服务里会放大尾延迟;应该离线固定或启动时设置。

CUDA 11.2+ 引入 stream-ordered allocator 解决这个问题:

// 主线程一次性创建 pool, 调好上限
    cudaMemPool_t pool;
    cudaDeviceGetDefaultMemPool(&pool, 0);
    size_t threshold = size_t(40) << 30;  // 40 GB
    cudaMemPoolSetAttribute(pool, cudaMemPoolAttrReleaseThreshold, &threshold);

    // 每请求所在 stream 上 allocate, 不会阻塞
    void* kv;
    cudaMallocAsync(&kv, kv_bytes, stream);
    // ... decode_step_kernel<<<..., stream>>>(kv, ...);
    cudaFreeAsync(kv, stream);   // 还是非阻塞, GPU 跑完才回收

关键好处:

vLLM / SGLang 这类推理框架会避免在 hot path 使用同步分配。实际 p99 改善幅度必须用目标 workload 压测确认,记录 QPS、batch 分布、KV 分配大小和 allocator 配置。

Per-request 显存与 cudaStreamAttachMemAsync

多租户场景下,多个请求共享 GPU。要保证某请求结束后 它的 KV 立即被回收给同 stream, 而不是等 pool flush,可用:

// 把 KV 标记成仅 stream 内可见
    cudaStreamAttachMemAsync(req_stream, kv, 0, cudaMemAttachSingle);
    // ... decode ...
    cudaFreeAsync(kv, req_stream);  // stream 完成立即回收

NVIDIA 官方 deep learning examples 也用同一招做 batch 异构调度,可参考 §5.2.3 的 CUDA_DEVICE_MAX_CONNECTIONS 环境变量来调整并发 stream 数。

下一章导览

下一章会把这些零件与前面手写的 CUDA kernel 串成 GPT-2 small 教学推理程序,并明确它与生产引擎之间的差距。