9.28 快手 AI Infra 一面:Attention 算子、profile 分析与 prefill/decode 调度、AWQ 量化
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
项目与部署经历
- 高性能 Attention 重构具体做了哪些工作?
- 部署过什么模型、参数量多大?
- 在项目中承担什么角色、具体负责哪一段?
- 你们使用的底层框架是什么,用在调用链的哪一层?
语言与 GPU 基础
- 讲讲你对 C++ 的理解,尤其是资源生命周期、内存布局、并发和 GPU 异步执行这几块。
- GPU 架构里 SM 和 Warp 分别是什么?它们怎样影响一个 kernel 的性能?
Attention 与算子优化
- Attention 算子主要有哪些优化点?
- 算子优化常见的两类瓶颈是什么?
- 是否了解 Roofline 屋顶线模型?
Triton 与性能分析
- 是否用过 Triton 写 kernel?解决了什么问题?
- 用过哪些 profile 性能分析工具,各自用来定位什么问题?
- 如何判断一个算子存在问题?
- 如何根据 profile 结果做算子优化?
推理调度与低时延
- 是否部署过小模型?低时延怎么做,重点说说 prefill 阶段。
- 新请求的 prefill 会不会踢掉正在运行的 decode 请求?
- prefill 与 decode 能否混合调度?
- 是否了解线性注意力?
量化
- 量化了解吗?AWQ 的原理是什么?
- W8A16 在 forward 中的执行流程是怎样的?
方向与手撕
- 意向方向是训练还是推理?
- 手撕算法:三数之和(两数之和的变种)。
《参考解析》
Attention 重构要落到四层改动和一份语义契约。 只说「用了 FlashAttention」说明不了重构,先交代负载:卡型、attention 类型、batch、query 与 KV 长度、head 数、head dim、dtype、是否 causal、是否带 KV cache。改动分四层。计算与访存层做分块和在线 softmax,把 scale、mask、dropout 融进同一个 kernel,避免把分数矩阵与概率矩阵写回 HBM——朴素实现的成本主要就花在这两个 n×n 中间量的往返读写上。并行与布局层定 tile、warp 数、流水级数,以及 Q/K/V 是分离还是按头交错、stride 是否连续、访存能否合并。形状与阶段层把 prefill(query 维度大、算力密)和 decode(单 token、以读历史 KV 为主)拆成不同 kernel 与配置,变长 batch 打包减少 padding。框架接入层要有算子注册、dispatch 规则、形状不满足时的回退和集成回归。最容易漏的是语义:query 与 KV 长度不等时,causal 三角 mask 是左上还是右下对齐会直接改变可见 token;GQA 的 query 头到 KV 头映射、padding mask、非连续 stride、输出 dtype、dropout 随机数是否可复现,都算接口的一部分,只在一个正方形 attention 上对一次输出等于没验。训练路径上,前向少存一个矩阵不代表反向免费,保存了哪些归一化统计、哪些量在 backward 重算、激活显存省了多少是用多少额外计算换的,都要能答上来。分块能成立靠的是在线 softmax:维护三个量——目前最大分数 m、以 m 为基准的指数和 l、未归一化输出 acc。读入新块分数 s 与 V,先算 m′ = max(m, max(s)),再把旧状态整体缩放 exp(m − m′),加上新块贡献:l ← l·exp(m − m′) + Σexp(s − m′),acc ← acc·exp(m − m′) + exp(s − m′)·V。比值 acc/l 在分子分母同乘正数后不变,所以结果与一次性 softmax 一致,差异只来自浮点结合顺序,累加器因此要留在 FP32。它不是近似,也没有丢掉小项。两处坑:被 mask 的位置要在求 max 之前排除,不能先 exp 再加;整块全被 mask 时 max(s) 是 −inf,m′ 也可能是 −inf,m − m′ 会算出 NaN,必须显式跳过没有有效列的块,整行被 mask 的 query 按接口约定给零输出。
Roofline 与两类瓶颈:先判断受限类型,再谈优化手段。 算术强度 I 是计算量比上某一级存储的搬运量,单位 FLOP/Byte,性能上界约为 min(峰值算力, I × 带宽),折点在 I* = 峰值算力 / 带宽。取 100 TFLOP/s 与 2 TB/s,折点是 50 FLOP/Byte,强度 10 的 kernel 带宽上限只有 20 TFLOP/s。用时有两个坑:字节数与带宽必须同一层级,按 HBM 算强度就不能拿 L2 带宽去比,权重实际驻留 L2 时「整层都从 HBM 读」的估法会高估流量、低估强度;FLOP 记法要统一,一次 FMA 算两个 FLOP,BF16 Tensor Core 峰值与 FP32 标量峰值差一个量级,混用会得出相反结论。经典两类是 compute-bound 与 memory-bound,但还有第三类:两者都不饱和,卡在 launch、同步、CPU 排队、串行依赖、并行度不足或通信上,此时调 tile、提 occupancy 都没用。判断靠算术强度加实测吞吐、流量、缓存命中与 stall,而不是算子名字——同一个 GEMM 在大 batch 和极小 batch 下不同区,同一 attention 的 prefill 与 decode 也不必同区。收益换算用 Amdahl:热点占端到端 f、算子加速 s 倍,整体加速 1 / ((1−f) + f/s);f = 0.3、s = 2 只有约 1.18 倍、端到端降 15%,所以「算子快两倍」不能写成「时延减半」,若这段还能与别的 kernel 重叠,收益再打折。
profile 按「回答什么问题」分三层用,结论要回到独立压测。 PyTorch Profiler 看框架层算子、CPU/GPU 活动与显存;Nsight Systems 看端到端时间线,CUDA API、kernel、memcpy、CPU 线程与 kernel 之间的空隙都在图上,可用 NVTX 标记 tokenize、prefill、decode 与通信;Nsight Compute 才下到单个 kernel,看吞吐、访存字节、occupancy、stall 与寄存器 spill。顺序是先采一小段真实负载的时间线定位热点,再对代表 kernel 收计数器;replay 会重放 kernel,采集时长不能当线上 p99。计时最常错在把异步当同步:CPU 侧返回不代表 kernel 结束,要用 CUDA events 包住目标区间,读时间前先等结束事件;warmup 要跑掉 JIT、autotune 与权重加载;形状、并发、测量区间固定,报分位数而不是均值。服务指标要分平均与尾部:平均 TPOT 会把一次停顿平均掉,得同时看 ITL 的间隔分布。判断「算子有问题」先分正确性与性能:shape、dtype、stride、mask、误差、NaN 先过一遍,训练路径还要看梯度,有越界或未定义行为先修,不能拿错结果比速度;性能要有可比基线(同输入、同精度、同 warmup),再看它是否在关键路径。「耗时最长」不等于有缺陷,它可能本来计算量最大;「GPU 利用率低」也不能证明某个算子有错。把现象变成可证伪的假设,例如「输出转置多了一次 HBM 往返」「head dim 太长导致 spill」「短 kernel 之间的空隙是 launch 开销」,再用计数器验证。动作上:HBM 流量接近可达带宽就融合、分块复用、降中间精度;Tensor Core 利用率低而矩阵工作占主导就调 tile、布局、换库;local memory 流量涨、寄存器用量高就减 live state、调展开;一堆短 kernel 有空隙就融合或用 CUDA Graph;有效字节少而事务多就改索引与布局。一次只改一个主要变量,同一套形状回归,否则长序列的最优 tile 会把短序列拖慢。
prefill 会不会「踢掉」decode,要拆成三件事看。 这轮有没有拿到执行机会、KV 还在不在显存、恢复要不要重算,三者不是一回事。token 预算和 KV 块都够时,调度器可以把 decode token 和一部分 prefill token 放进同一轮,也可能先服务一类让另一类等下一轮——等一轮不代表 KV 丢了。真正涉及资源的是 KV 块不足:引擎可以延后新请求、限制准入,也可以按策略抢占已有请求并释放 KV,恢复时重算或换出到 CPU,支持哪种取决于引擎与配置。被抢占直接表现为 TPOT 抖动,频繁发生通常说明容量或并发配置不合理,而不是某个 kernel 慢。GPU 层是另一回事:新请求进应用队列不会中断已提交的 kernel,长 prefill 在跑,decode 就得等它跑完,软件层写「decode 优先」不等于硬件能抢占。排查时把等待队列、每轮 token 预算、可用 KV 块、抢占与重算计数一起看,再对齐 TPOT。混合调度可行:continuous batching 让批成员逐轮变化,chunked prefill 把长 prompt 拆成多轮。一轮预算 512 个 token,100 个已接纳请求各要 1 个 decode token,剩 412 给 prefill chunk;这是 token 预算不是毫秒预算,同样 512 个 token 在不同上下文下耗时差很多。chunk 调小 decode 少等,但调度与 launch 开销上升、prefill 效率下降;调大则 prefill 吞吐好看、TPOT 变差。评测固定到达率与长度分布,联合看 TTFT、TPOT 与 goodput(同时满足时延 SLO 的完成请求数每秒),并确认长请求不被饿死。P/D 分离是另一种架构,分池后要传 KV,网络带宽和副本配比成了新问题;PagedAttention 只解决碎片,物理 KV 容量仍有限,不能因此不做准入控制。
小模型低时延,固定开销和权重带宽都会咬人。 参数少意味着单次计算短,tokenize、CPU 调度、kernel launch、同步、H2D/D2H 的占比反而上升,不保证尾延迟低。TTFT 从请求到达开始拆:排队、预处理、prefill、采样、返回,流式网络算不算要先把口径说清。短输入低并发先查算子融合、CUDA Graph、固定缓冲区与 CPU 热路径——CUDA Graph 把一串 launch 录成图,代价是形状和地址固定,动态请求得分桶或重捕获;热路径上的 .item()、.cpu()、同步日志都是常见元凶。长输入看 GEMM 效率、attention 的 IO、KV 写入、padding 浪费与分块策略。共享前缀可用 prefix cache,但复用要 token 级一致(同模型、同 adapter、同位置、同注意力实现),按语义相似复用 KV 是错的。量化也要配专用 kernel,短小计算里反量化开销可能超过省下的带宽。为什么短 prompt 离峰值很远,用一次矩阵乘估:FLOPs 约 2MNK,理想字节 (MK + KN + MN)·b,强度约 2MNK / ((MK + KN + MN)·b);M 很小、KN 占主导时约等于 2M/b,与 M 成正比。凑 batch 能摊薄权重读取,但等待凑批会推高首 token 延迟,是取舍不是免费优化。显存预算算四块:权重、KV cache、激活与 workspace、框架预留。7B 稠密模型 16 位权重约 7×10⁹×2 字节 ≈ 13 GiB,这只是权重;KV cache 约 2·L·T·H_kv·d·b 字节,GQA、MLA、滑窗按真实状态布局重算。压测要用固定到达率的开环,闭环「上一个完成再发下一个」会在系统变慢时自动降压,把排队掩盖掉。
AWQ 用激活幅度决定哪些权重列不能量化得太糙。 不是所有权重同等重要:某些输入通道的激活天生幅度大,对应权重列的量化误差会被直接放大到输出。做法是用校准集统计输入通道的激活尺度,选一组通道缩放 s,权重列乘 s、输入对应通道除 s——浮点下这是恒等变换(y = W·diag(s) · diag(s)⁻¹·x),但量化非线性,改尺度就改了每个权重的量化步长,等于把误差在通道间重新分配。s 靠搜索加比较量化前后的模块输出误差来定,不需要梯度反传,与 GPTQ 的逐列重构不是一回事;重要通道仍然量化,只是量化得更细,不是挑出来留一份 FP16。坑在 per-group 共享一个 scale:某一列放大过头会把整个 group 的量化步长抬高,反而伤同组其它列,所以不是越大越好;缩放还要能被相邻层吸收(前一个 layernorm 或线性层),跨残差和非线性不能随便搬。评估看困惑度和业务指标,只看权重误差没有意义;group size、bit 数与 kernel 支持共同决定收益,权重压下来不等于服务变快。
W8A16 的 forward 是「离线打包、在线融合反量化」。 它指权重 8 位、激活 16 位,通常落到 INT8 权重配 FP16/BF16 激活的 weight-only 量化;若指 FP8 权重,格式与硬件支持要另外确认。离线:浮点权重 → 量化 → qweight 加 scale(非对称还有 zero point)→ 按 kernel 要求预打包成交错布局。在线:读 A16 激活与一块 W8 权重 → 在寄存器或片上做反量化(int8 乘 scale、加 zero)→ 矩阵乘,常见是 FP16/BF16 相乘、FP32 累加 → epilogue 处理 bias、残差与输出 dtype。最差的写法是先把整层权重反量化成 FP16 再调通用 GEMM,多出一份临时权重和一次额外读写,权重带宽省下的又花回去,所以反量化必须按块融进内循环。scale 粒度决定能否提到循环外:按输出通道的可以放 epilogue 统一乘,按输入维分组的每个 group 不同,只能块内逐段处理,这正是「都叫 W8A16 但 kernel 不一样」的原因。它不是 W8A8,激活没量化,套「激活量化 → INT8 乘 → 重标定」是错的;KV cache 也不会自动跟着量化。收益落在权重带宽主导的 decode,大 batch prefill 偏计算受限,加上反解压与布局转换未必更快。压缩比可以顺手算:每 128 个权重配一个 FP16 scale,INT8 数据加 scale 约 130 字节,BF16 原权重 256 字节,约 1.97 倍,还没算 zero point、padding 与打包元数据。
线性注意力换掉的是表达形式,不是实现技巧。 用核特征映射 φ 重写注意力:输出等于 Σφ(q)·φ(k)·v 除以 Σφ(q)·φ(k)。令 S = Σφ(k)vᵀ、z = Σφ(k),因果推理时两者可逐 token 递推,不必保存全部历史 KV,状态大小 r×d_v,处理 n 个 token 约 O(n·r·d_v),对 n 是线性的。这个「线性」只针对序列长度:状态元素数是 r×d_v×头数×层数,上下文短时不一定比 KV cache 小,存在交叉点。局限是用固定大小的状态压缩历史,精确检索与长程细节会退化,所以常见做法是混合层,多数层线性、少数层保留标准 attention。门控、衰减、delta rule 的更新式各不相同,一个递推式概括不了全部,而且需要从头训练,把训好的 softmax 模型换掉不能默认质量不变。它和 FlashAttention 也不在一个层次:后者优化标准 attention 的 IO,计算量仍是平方。验证递推可取非负特征映射,直接算完整因果核矩阵再与 S、z 对比,但这只证明实现正确,不证明与 softmax 等价。