# WeiGao Systems Wiki > 记录 AI 系统、底层计算与工程实践的个人知识库,连接论文、源码、trace 与性能建模。 本文件按时间倒序收录 192 篇文章的正文。索引见 https://weigao.cc/llms.txt。 --- ## Hyperloom Specialist 与配置调优:一个 Agentic 调参系统的源码拆解 > Source: https://weigao.cc/ai-systems/llm-inference/hyperloom-specialist-config-tuning/ > Date: 2026-08-05 > Tags: llm-inference, gpu-optimization, agentic, hyperparameter-tuning, benchmark, rocm, sglang, vllm 这是一份对 [AMD-AGI/Hyperloom](https://github.com/AMD-AGI/Hyperloom) 的源码级拆解,读的是 2026-08-04 的主干快照(v1.0.0a2,MIT)。结论带 `文件:行号`,从代码而非 README 得出;README 与实现不一致的地方单独标注。本文不含任何性能承诺——主要结论之一恰恰是:这套系统报出的收益数字不能当测量值读。 :::important[30 秒复习] - **一句话**:Hyperloom 的调参不是搜索算法,是「LLM 提议 flag + 确定性代码把关」。提议侧没有参数空间模型,把关侧的工程质量很高但统计学是空的。 - **三个判断**:knob 完全无类型(自由字符串,不校验合法值);接受阈值按 `0.1 + 0.9/N` 逐 cycle 衰减,第 10 个 cycle 降到 0.19%,穿到了代码自己声称的噪声地板之下;每个变体只测一次,全仓唯一带重复采样和显著性检验的工具没有任何调用方。 - **来源问题**:这份源码回答一个 agentic 调参系统的 knob 面从哪来、变体如何去重与验收、以及收益数字能被当作测量值到什么程度。 - **值得抄的**:禁止 agent 自报加速比(机器强制)、内容寻址指纹去重、冷热轮分离、12 类失败分类学、KEEP 后二次复测。 - **边界**:静态阅读,未实际运行过;跨运行知识沉淀有多处字段投影丢数据,「self-evolving」在 OSS 版本里基本不成立。 ::: ## 1. 范围与术语 Hyperloom 的整体流水线是 Magpie 采 trace → TraceLens 出 roofline 目标 → Arbor 做搜索 → GEAK 优化 kernel。方法论背景见 [Agentic Infra:LLM 推理性能优化](https://weigao.cc/ai-systems/llm-inference/agentic-infra-inference-optimization/)。 本文只拆两件事:**specialist 子系统怎么实现**,以及**服务配置的超参数调优怎么做**。kernel 生成(GEAK)和 trace 分析(TraceLens)不在范围内。 术语对齐:这里的「超参数」不是训练超参,而是**推理服务的配置量**——server 启动参数(`--max-num-seqs`、`--kv-cache-dtype`)和环境变量(`VLLM_ROCM_USE_AITER`、`HSA_ENABLE_SDMA`)。代码里管一次配置改动叫 **variant**(变体),管一个可调项叫 **knob** 或 **lever**。 ## 2. 可调什么:knob 面 ### 没有 knob 注册表 这是理解整套设计的前提:**Hyperloom 没有任何 per-knob 的类型定义**。没有名字表,没有类型,没有合法区间,没有默认值,没有「这个 knob 属于哪个框架」,没有「改了要不要重启」。 一次配置改动的完整载体就是这个 dataclass(`actions/executors/_grid_base.py:73`): ```python @dataclass class GridVariant: name: str extra_server_args: str = "" # 自由字符串,LLM 直接写 extra_envs: dict[str, str] = field(default_factory=dict) remove_args: list[str] = field(default_factory=list) unset_envs: list[str] = field(default_factory=list) args_mode: str = "append" # 或 "replace" note: str = "" ``` 对 knob 的校验只有结构层,没有语义层: - 参数串必须能被 `shlex` 切开、不含 shell 元字符、必须是 flag 形状(`_grid_server_args.py:32`) - 环境变量**键**必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$` 且不在 secret 名单里(`common/env_safety.py:57`) - 环境变量**值**只做 `str(value)`,**从不做区间检查** `--max-num-seqs 999999` 和 `VLLM_ROCM_USE_AITER=maybe` 都会被原样送进去,靠 server 启动失败来发现。这是有意的取舍:失败带 `error_class` 记进台账让 LLM 下次别再选,代价是烧掉一次 benchmark。 真正花了工夫的地方是**容忍 LLM 输出不规范**:`_coerce_args_str`(`explore.py:112`)接受 JSON 数组并空格拼接;`coerce_extra_envs`(`_grid_base.py:160`)接受 dict、`"FOO=1 BAR=2"` 字符串、`["FOO=1"]` 列表、`[{"FOO":"1"}]` 列表套字典四种形状;`split_config_changes`(`_grid_server_args.py:190`)按前导 `-` 把一个扁平 dict 拆成 args 和 envs。 ### knob 词汇表散在五处 | 来源 | 位置 | 内容 | |---|---|---| | 种子网格 | `explore.py:262`(atom)、`:350`(xdit) | 只有 atom / xdit 有代码定义的初始网格;**sglang 和 vLLM 返回 `[]`**,冷启动完全依赖 LLM,无输入时直接 `empty_grid` 失败 | | Prompt focus 块 | `prompts/specialist_prompt_builder.py:670` | 每个 domain 一段 Markdown 散文,knob 名字写在文字里 | | 强制注入 / 守护 | `_grid_server_args.py`、`_workload_envs.py` | 不是候选而是硬默认,如 `--watchdog-timeout 1800`、MoE 中间维非 128 对齐时剥掉 `--attention-backend aiter` | | 精度风险名单 | `_accuracy_gate.py:336` | 5 个 CLI 子串 + 10 个环境变量键,只有命中的才跑精度门禁 | | 黑名单 | `_grid_variant_filter.py:309` | xDiT 的已知崩溃组合,**但这个过滤器在生产路径无调用方** | prompt 里的 knob 是散文形式的。举个真实例子(`specialist_prompt_builder.py:251`,kernel_switch domain):SGLang 的 `--attention-backend` 枚举 `ROCM_AITER_MLA` / `TRITON_MLA` / `ROCM_AITER_TRITON_MLA`,vLLM 的枚举 `ROCM_ATTN` / `ROCM_AITER_FA` / `ROCM_AITER_UNIFIED_ATTN` / `FLASH_ATTN`,还专门标了一个陷阱值 `ROCM_FLASH`(无效)。**这些知识只存在于 prompt 文本里,代码不知道。** serving domain 那一段还带 ALWAYS_ON / NEVER_TOUCH 标注(`VLLM_ROCM_USE_AITER=1` 常开,`VLLM_ROCM_USE_AITER_RMSNORM=0` 别动),以及反向 knob(MLA + FP8 上别开 `torch.compile`)。这套东西本质是把一份人类调参手册塞进 prompt。 覆盖到的类别:批处理调度(`--max-num-seqs`、`--max-num-batched-tokens`、`--enable-chunked-prefill`)、KV/显存(`--kv-cache-dtype fp8_e4m3`、`block_size ≥ 16`、`--gpu-memory-utilization`)、attention 后端、编译与图捕获(`--compilation-config`、`--enforce-eager`、`--cudagraph-capture-sizes`)、通信(`VLLM_ROCM_QUICK_REDUCE_QUANTIZATION=INT4`、`NCCL_MIN/MAX_NCHANNELS`)、驱动与系统(`HSA_ENABLE_SDMA=0`、`HIP_HIDDEN_FREE_MEM`、`numactl --cpunodebind`)。这些 knob 各自的语义见[批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/)和[推理 Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/)。 `SKILL.md:897` 还列了一批「specialist 可以自行提出」的候选族(`--disable-radix-cache`、`--max-running-requests`、`--stream-interval`、`SGLANG_OPT_USE_MULTI_STREAM_OVERLAP`),这些**只在文档里,代码完全不知道**。 ### 框架覆盖与跨框架翻译 `framework_registry.py:62` 是一张五行表:`sglang`(默认)、`vllm`、`atom` 是 serving 类;`xdit`、`hunyuan_image3` 是 scriptable 类(无 server,单命令,用 LPIPS/SSIM 图像质量门禁替代精度评测,排序指标翻转成 `e2el_mean_ms`)。 框架分派靠**字符串子串匹配**,而且判据是「解析出来的环境变量名」而不是框架本身——`if server_args_env_name(framework) != "EXTRA_SGLANG_ARGS": return args` 这个模式在 `_grid_server_args.py` 里出现 5 次(592、725、797、920、989)。未识别的框架串会**静默按 sglang 处理**。 跨框架 knob 拼写差异只在一处硬编码特例里被处理(`_workload_envs.py:1020`): ```python _mimo_is_vllm = "vllm" in str(bench.get("framework") or "").lower() # sglang accepts lowercase `triton`; vLLM only knows TRITON_ATTN. _mimo_attn_backend = "TRITON_ATTN" if _mimo_is_vllm else "triton" ``` 没有通用翻译层。所以在 sglang 会话里提一个 vLLM 专属 flag,会被原样写进 `EXTRA_SGLANG_ARGS`,然后 sglang 的 argparse 在启动时报错——烧掉一次 benchmark。框架间实现差异为什么难做成通用层,见[推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/)。 ## 3. specialist:提案的生产者 ### 10 + 1 个 domain `specialists/domains.py:73` 是一个硬编码的 10 项 tuple,外加一个合成的 `freeform_specialist`: | key | 负责层 | kb_anchor | |---|---|---| | `serving_specialist` | sglang/vllm scheduler、cuda_graph、kv_cache、chunked prefill | framework | | `kernel_switch_specialist` | aiter / sglang kernels / triton,attention、MoE、GEMM | kernel_agent | | `comm_specialist` | RCCL/NCCL、AllReduce、QuickReduce、拓扑 | communication | | `compiler_specialist` | torch.compile、inductor、AMDGCN、寄存器压力 | compiler | | `system_specialist` | KFD/driver、launch latency、dispatch 开销、numactl | systems | | `pr_intel_specialist` | 跨仓库 PR 调研,给其他 specialist 喂 ref | pr_intelligence | | `research_scout_specialist` | 只读:参考脚本、config.json 架构特性、NVIDIA PR/MLPerf | research_scout | | `static_recon_specialist` | 只读:grep 源码找被 predicate 静默关掉的快路径 | static_recon | | `enablement_specialist` | 让跑不起来/精度不过的组合能跑对,可改到 /opt/rocm、HIP、aiter | framework | | `cross_framework_rewrite_specialist` | sglang ↔ vllm 特性移植,重写而非 git-apply | framework | **domain 只是一个字符串。**它唯一的作用是选一段 prompt focus 模板和一个 KB anchor 标签,没有任何 per-domain 代码。README 说的「Dynamic Specialist Agent」就是这个——不是学出来的 agent,也没有动态合成。 ### 一个 specialist 就是一个 claude CLI 子进程 这是最值得注意的实现事实(`specialists/subprocess_.py:705`): ``` claude --print --output-format stream-json --verbose --permission-mode bypassPermissions --system-prompt-file /prompt.md -p "Execute the task in your system prompt. Work autonomously. Write specialist_done.json as your absolute last action." --allowedTools --add-dir --add-dir ``` - **隔离**:`git worktree add -b specialist-`,真 worktree 不是拷贝。找不到 git root 时会**降级为无写隔离**继续跑(`runner.py:1378`) - **权限**:`bypassPermissions` 是默认值(`subprocess_.py:166`)。源码注释给了理由——claude-cli 的安全分类器依赖另一次网关调用,降级时曾经「96 次 classifier-unavailable vs 6 次成功运行」,直接烧完预算 - **Bash 不过滤**。`runner.py:98` 原话:*"Bash is granted unfiltered — there is no per-call filter. Safety rests on the isolated git worktree, this allowlist, and Critic + PolicyGate review."* `SPECIALIST_TOOL_DENYLIST` 是空集 - **并发数 = 2 × GPU 数**(`policy/gate.py:205`),探不到 GPU 时兜底 2。8 卡 MI300X → 16 个并发 CPU specialist;但要 GPU 的 specialist 走 `gpu_research_lane`,容量是 **1**,严格串行 - **`--max-turns` 从不传**。`specialist.yaml` 里写的 `max_turns: 12` 不生效,运行时默认 1000(`domains.py:359`),真正的停止条件是墙钟:`min(base × (cycle+1), 240min)`,base = 10min(CPU)/ 60min(GPU),带 bench 的地板 140min - **存活判定**:每 5s 轮询,`heartbeat.json` 或 `process.log` 的 mtime 超 300s 未更新才收割,实际约 10 分钟静默才杀 - prompt 规模实测 ~4–7k token,大头是 13–15KB 的静态 domain playbook,不是遥测数据 自动重试 `SPECIALIST_AUTO_RETRY_MAX = 2`(`loop/coordinator.py:27`),只对 TIMEOUT / STALE_HEARTBEAT / CRASH 三种基础设施故障重试;语义失败(空提案、工具违规)不重试。注意 specialist executor **从不抛异常**——每条退出路径都合成一个 `specialist_done` 载荷,所以 `SubAgentResult.state` 永远是 `succeeded`,真实结果藏在 `result["runner_status"]` 里。 ### domain 选择:一个真实的评分函数,但纯咨询性 `phases/explore.py:134` 的 `_plan_cycle_focus` 是确定性打分,结构上能看出探索-利用的意图: | 信号 | 权重 | |---|---| | 匹配当前 `bottleneck_shift.to_domain` | +5.0 | | 该方向已在 roofline 内饱和 | −100.0 | | 未饱和 | +1.0 | | 历史 cycle `gain_delta` | clamp(−2.0, +3.0) | | 还没当过 cycle focus(探索奖励) | +1.5 | | 近期负台账计数 | −min(4.0, 0.5·count) | 两个问题: 1. **候选集只有 5 个 domain**。来源是 `BOTTLENECK_DOMAIN_HINTS`(`kernel/roofline_snapshot.py:659`,5 个方向映射到 4 个 domain)加 freeform。`compiler_specialist`、`enablement_specialist`、`cross_framework_rewrite_specialist` 等 6 个**永远不会被选为 cycle focus**,只能靠 LLM 直接派或内部 enqueue。 2. **负反馈那一项是断的**。`_negative_ledger_domain_counts` 按 `domain` / `specialist_domain` / `source_domain` / `provenance` 取 key,但写入 `explore_search` 台账的行**只有 `provenance`**,取值是 `llm_direct` / `default_grid` / `specialist:`——没一个是 domain key。惩罚全落到凭空造出来的 key 上,5 个真实候选一分不扣。 而且 focus 的输出是**纯咨询性**的:PolicyGate 的 specialist 门禁完全不读它,prompt 里那行字面写着 `"Advisory only: use this as a prior, not a dispatch gate."` 唯一确定性的强制派发是「停滞 domain 兜底」(`phases/explore.py:533`):per-anchor 计数器 `rounds_since_last_specialist ≥ 8` 或 `rounds_since_last_keep ≥ 12`(`coordinator.py:258`)时强制派一个,每 tick 最多一次,选 gap 的规则是最高严重度 → 尝试最少 → 最旧。这条是真的轮转覆盖保证——任何知识域不会被饿超过 8 轮。 ### 提案的质量控制:禁止自报数字 这是整套系统里最该抄的一条。`patch_safety.py:33`: ```python FORBIDDEN_PROPOSAL_FIELDS = { "expected_gain", "expected_gain_pct", "bench_evidence", "confidence", "score", "rank", "force_provenance", } ``` 外加 4 条正则扫自由文本里的 `%` / `x` / `ms|us|tok/s|qps|tps` / `speedup of N`(`:47`)。prompt 里也明确写「the Coordinator measures gain」。禁用字段是硬信号,数字声明是 advisory 警告——两者都不丢弃提案,但都进 `notes` 留审计痕。 提案还有个 4 模型 LLM 集成打分器(`scoring/proposal_scorer.py`,claude-opus-4-8 / gpt-5.5 / Kimi-K2.6 / gemini-3.1-pro),输出 0–10 整数,最多打 16 条。它是**结构性咨询**的:渲染时把模型名替换成 `rater_1..rater_N`、不算均值、不排序,prompt 里写「Advisory only — one reference among many, NOT a ranking directive」,还叮嘱「不要猜 rater_N 是哪个模型」「评分者之间的分歧本身是不确定性信号」。唯一的非 prompt 用途是 Langfuse 里的预测-实测校准遥测。 ## 4. 从提案到可运行变体 ### 指纹:内容寻址,但不含 workload `_canonical_fingerprint.py:64`,16 位 SHA-1: ```python args_tokens = sorted(shlex.split(args_text)) env_pairs = sorted((str(k), str(v)) for k, v in (extra_envs or {}).items()) # remove_args / unset_envs / args_mode / runtime_override 只在非默认时才进 payload payload_obj = [args_tokens, [list(p) for p in env_pairs]] ``` **故意排除**:`name` 和 `note`(改名不影响去重,这个设计对)、`framework`、`tp`、`workload_signature`。 最后一项是真问题。`workload_signature`(CONC/ISL/OSL/PRECISION/TP 的 12 位摘要)只作为旁路元数据存着,**不进哈希**。后果是:一个在 `CONC=64` 测过的变体,会把 `CONC=256` 下的同一变体一起挡掉。对任何要扫 workload 形状的场景,这是硬伤。 还有个次要的归一化损失:排序后 `--a 1 --b 2` 和 `--a 2 --b 1` 都变成 `['--a','--b','1','2']`,会碰撞。 ### `args_mode` 是黏性的 `compose_server_args`(`_grid_server_args.py:171`)两种模式: - **append**(默认):`remove_args` 只作用于 `inherited + base`,变体自己的参数不被剪 - **replace**:`inherited_args` 整个丢掉,`remove_args` 同时作用于 base **和变体自身**——这是不对称的,LLM 要是把自己的参数写进 `remove_args`,会静默删掉自己的改动 更麻烦的是黏性(`explore.py:1378`、`:1541`):**一旦某个变体用了 `remove_args` 或 `replace`,本轮剩下的所有变体都切到 replace 模式**,YAML 继承的参数对后续每个变体都被丢掉。 冲突参数靠三道「后者胜」来收:`merge_server_args` 故意不去重(「重复 flag 就是后者覆盖前者的手段」),然后 `_shell_safe_dedupe` 对所有框架跑一遍,`dedup_vllm_server_args` 只对 vLLM/atom 跑(sglang 是 no-op,因为 vLLM 对重复 flag 会硬报错)。两个去重器只要字符串里出现任何 JSON 值的 flag(`--compilation-config`、`--hf-overrides`、`--speculative-config` 等 7 个)就**整体放弃**,此时任意 flag 的重复都会存活。 ### 物化:Hyperloom 默认不写 server 命令行 默认后端只产出 Magpie YAML(`benchmark_backend.py:79`),参数落在一个环境变量里(`EXTRA_SGLANG_ARGS` 等)。真正的 server 命令行由外部 Magpie/InferenceX shell 脚本拼,而且 **`$EXTRA_*_ARGS` 是不加引号展开的**。 这一个决定制造了大量偶然复杂度:`compact_json_server_args`、`_reserialize_json_blobs`、`_repair_unquoted_json`、`_SPACE_VALUE_FLAGS`、两个去重器——全部只为了伺候这个 unquoted splice。改成传 argv 数组(bypass 后端就是这么做的),这几百行可以全删。 workload 形状的映射在 `_workload_envs.py`:`CONC` / `ISL` / `OSL` / `MAX_MODEL_LEN` / `TP` 从环境变量读入,TP 会**按可见 GPU 数自动夹取**;客户端负载按序列成本分档推导(`ISL+OSL ≤ 1024` → `NUM_PROMPTS = CONC×10`,`≤4096` → ×5,`≤16384` → ×3,更大 → ×2),`NUM_WARMUPS = min(CONC, 8)`。 ### 运行前校验:只有一个真探针 | 检查 | 触发条件 | 位置 | |---|---|---| | `validate_server_args_shell_safe` | shell 元字符、切不开、裸位置参数 | `_grid_server_args.py:32` | | `unsupported_capability_reason` | 唯一的真 build 探针,30s 子进程 | `_grid_runner.py:315` | | sweep 组合剔除 | `ISL+OSL > MAX_MODEL_LEN` | `sweep.py:108` | | TP 夹取 | `TP > 可见 GPU 数` | `_workload_envs.py:621` | | 模型闸门 | 15 个检测器的瀑布:vision-only、不支持的量化、rope 异常等 | `model_gate.py:1648` | 那个「唯一的真探针」只覆盖**一个环境变量、一个框架**: ```python fw = (os.environ.get("FRAMEWORK", "") or "sglang").strip().lower() if fw != "vllm": return None val = envs.get("VLLM_ROCM_USE_AITER_FUSION_SHARED_EXPERTS") ``` 它有一个细节做得对:探针返回 `unknown` 时**既不丢弃也不缓存**——不确定就别拦。 **没有任何探针检查「提议的 flag 是否存在」或「值是否合法」。** ### 三张死掉的安全网 `_grid_variant_filter.py` 里有六个过滤器,只有两个接进了生产路径(多节点无效变体、aiter MoE pin)。以下全部只有测试调用: - `apply_compatibility_filter` — 连带整个 xDiT 崩溃黑名单(`TRITON_HIP_USE_ASYNC_COPY` 在 gfx950 上崩、`AMD_DIRECT_DISPATCH=1` + `AMDGCN_USE_BUFFER_OPS=1` 已知 −28.6%、`NCCL_PROTO=LL` 回退 10–22%)和 `--help` 版本探针 - `apply_user_skip_list` + `resolve_skip_spec` — `--skip-variants` CLI flag 一路传到 `$SKIP_VARIANTS` 和多节点子进程,**但没有任何代码读回来**;help 文本承诺的 `state.json` 里的 `dropped_variants` 字段,全仓只有那句 help 文本本身 - `_probe_server_help_text` — 这是唯一能提前发现「flag 在当前框架版本里不存在」的机制 而 `explore.py:361` 的 docstring 明确写着「已知回退/崩溃 knob 在这里省略,并额外由 `_grid_runner.py` 的 `xdit_blacklist_reason` 强制」。那张安全网不运行。 ## 5. 搜索算法:贪心爬山 + 衰减接受线 ### 分类:不是任何一种经典优化器 代码里没有代理模型、没有采集函数、没有种群、没有 reward 后验、没有 UCB/Thompson 项。实际形态是:**LLM 提议 flag,串行贪心叠加,确定性阈值判收**。 分工很清楚。LLM 负责生成候选,它看到的全是 prompt 文本:gap 台账、TraceLens 的 `analysis.md`、4 模型集成的咨询打分、plateau 提示、当前接受阈值、可重测清单。确定性代码负责其余一切——去重、串行运行、冷热轮纪律、KEEP/REVERT 判定、二次复测、精度门禁、相位机。 ### 关键机制:贪心叠加(有序依赖) `explore.py:1293` 拿每个变体和 `running_base_tput` 比,而这个值**每次确认 KEEP 后就地更新**(`:1542`)。所以同一轮里第 k+1 个变体是**叠在**第 k 个变体的配置之上测的。后果: - **顺序依赖**:LLM 给出的网格顺序(多节点还会被 `reorder_grid_for_multi_node` 重排)会改变哪些变体被留下。代码不做任何顺序纠偏。 - **无交互搜索**:`synergy_attempted` 这个台账字段存在、被读、被回传,**但没有任何写入方**。组合效应完全交给 LLM 去提一个合并变体。 - 设计规则明确写了一次只改一个(`explore.py:20`,理由是单租户 serving GPU)。 ### 衰减接受曲线 这是整套设计里最值得单独讲的一处(`phases/machine_state.py:429`,已逐行核实): ```python # Decaying acceptance curve: the marginal-gain bar shrinks each macro-cycle. The # KEEP threshold, stack-stable threshold (=keep/2) and convergence gain bar all # ride this single curve. KEEP_THRESHOLD_FLOOR_PCT: float = 0.1 KEEP_THRESHOLD_SPAN_PCT: float = 0.9 # Multi-node baseline noise floor is ~2x single-node; scale the curve to match. MULTI_NODE_KEEP_THRESHOLD_FACTOR: float = 2.0 def decaying_keep_threshold_pct(macro_cycle, *, multi_node=False): n = max(1, int(macro_cycle) + 1) base = KEEP_THRESHOLD_FLOOR_PCT + KEEP_THRESHOLD_SPAN_PCT / n return base * MULTI_NODE_KEEP_THRESHOLD_FACTOR if multi_node else base ``` 注入点 `loop/proposals.py:371`,同时设定 `stack_stable_threshold_pct = keep / 2.0`。 | macro_cycle N | KEEP 阈值 | 二次复测地板 | |---|---|---| | 1 | 1.00% | 0.50% | | 2 | 0.55% | 0.275% | | 3 | 0.40% | 0.20% | | 10 | 0.19% | 0.095% | | → ∞ | 0.10%(地板) | 0.05% | 同一条曲线同时驱动 KEEP 门槛、复测稳定性地板和每 cycle 收敛判据。 这条曲线的**意图**是合理的:越往后越只剩边际收益,台账里旧的次阈值变体会随着门槛下降重新可测(`_is_blocked` 在 `explore.py:826` 按 `prior_gain < keep_threshold_pct` 解锁)。**问题是它和噪声地板的关系**——见 §6。 ### 去重台账 - `tested: dict[fingerprint → entry]`,上限 `_EXPLORE_TESTED_CAP = 5000`,按插入序淘汰最旧 - `rejected: list`,按指纹去重,**无上限** - `accepted: list`,唯一写入方是 `record_explore_accepted` - 每行在合并时打上 `cycle` 和 `bottleneck` 戳 阻塞规则(`explore.py:810`):`KEEP` / `FAILED` / `KILLED_OVERTIME` 永久阻塞;`REVERT` / `KEEP_UNSTABLE` 随阈值衰减解锁,解锁清单会作为「Re-testable now」进 prompt。 这里有个缺陷:**`KEEP_UNSTABLE` 永远不会重新阻塞**。它记录的 `gain_pct` 是复测前的决策轮增益,按定义已经过了 KEEP 门槛,所以 `prior_gain < keep_threshold_pct` 恒为假 → 永久可重测,而门槛只会继续降。一个复测不稳定的变体,恰恰是最不该再烧 2–3 次 benchmark 的那个。 ### 并发度:严格串行 `explore.py:1008` 是 `for idx, gv in enumerate(runnable)` 顺序 await;`_grid_runner.py:1214` 同样。一轮里一次只有一个 server。注释解释了原因(`explore.py:996`):per-variant `ray.kill` 会搞崩 raylet,所以一个 Ray serving lease 跨整轮,变体之间靠 driver 侧 pgid kill 收服务。 每个变体最多 3 次 benchmark:丢弃的冷启动 warmup、决策轮、KEEP 后的复测轮。6 个变体 2 个 KEEP 的一轮 = 14 次 server 侧 benchmark pass,全串行。 每变体的硬超时是 `baseline_runtime_sec × (kill_ratio + 0.5)`,夹在 `[2400, 14400]` 秒;软超时 `decision_anchor_sec × 2.0`,从 server-ready 开始算。 ### 停滞与收敛 | 机制 | 判据 | 常量位置 | |---|---|---| | EXPLORE plateau(唯一确定性相位推进) | 最近 5 个 winner 增益和 < 0.5% **且** 尾部空 specialist 轮 ≥ 5 | `machine_state.py:363` | | EXPLORE 硬退出(覆盖 plateau) | 会话剩余 ≤ 3h **或** 相位剩余比 ≤ 20% | `:372` | | 全局收敛 | 连续 3 个 cycle 无增益 → 终止 | `:420` | | 方向饱和 | `within_roofline_pct ≥ 95%` | `roofline_snapshot.py:28` | 注意 EXPLORE plateau 是个**严格的 AND**:只要 LLM 还在产出非空 specialist 轮,无论增益多平,plateau 都不触发。真正结束一次平坦运行的是硬时间闸门,不是 plateau。 方向饱和只在一个地方真正门控行为:`should_reloop_to_explore` 要求 `saturated_directions` 的**每一项**都饱和才阻止 reloop。但这个 dict 只会为「曾经成为主导方向」的项累积 key,典型情况下只有一个 key——所以单次 95% 快照就能阻止 reloop。其他地方饱和都只是 prompt 文本。**没有任何地方因为方向饱和而剪掉一个变体或一族 flag。** roofline 饱和判据背后的模型见 [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/)。另有一处小 hack:`dominant_direction` 在 `roofline_bound_kind == "memory"` 时注入一个 `max(compute_pct, 0) + 0.01` 的合成候选(`roofline_snapshot.py:692`),所以只要 roofline 判 memory-bound,`memory` 就必然胜出,无视实测的 comm/idle 百分比——一个 comm 主导但 memory-bound 的负载会被路由到 `serving_specialist` 而不是 `comm_specialist`。 ## 6. 测量方法学 这是最要命的部分。前面的工程质量很高,这里是空的。 ### 优化目标:单目标吞吐 优化的标量是 `throughput.output_throughput`(输出 tok/s),`benchmark_result.py:745` 抽取,`gain_pct = (new-base)/base*100` 是**唯一**被拿去和阈值比的量。 `request_throughput`、`ttft_mean_ms`、`ttft_p99_ms`、`tpot_mean_ms`、`e2el_mean_ms`、`e2el_p99_ms` 全都解析了、存了、进报告了,**但没有任何决策读它们**。全仓没有 goodput、没有 SLO、没有 MFU。 用户能控制的只有**停止条件**(`TARGET_GAIN_PCT` / `TARGET_TPUT_PER_GPU` / `TARGET_DIR`),不是排序指标。你没法让 Hyperloom 在吞吐下界约束下优化 P99 TTFT。 ### 单次测量,无重复,无显著性检验 每个变体的决策只依赖**一个** benchmark 窗口。 | 通道 | 每变体轮数 | 谁定生死 | 统计量 | |---|---|---|---| | `explore`(配置调优) | warmup(丢弃)→ 决策 → 复测 | 决策轮定 KEEP,复测轮替换掉头条数字 | 无 | | `integrate_patch`(补丁) | bench → 复测 | bench 定 KEEP | 无 | | baseline | warmup(丢弃)→ 测量 | 测量轮 | 无 | 变体确实测了两次,但**两次从不合并**:第一次管准入,第二次覆盖第一次成为头条。没有均值、没有中位数、没有离散度。`run_grid` 里不存在任何重复循环。 **冷热分离做得是对的**。冷轮在三个层次被真正丢弃:baseline 双跑(`baseline.py:2117`,日志还会打印「冷产物本来会是 +X%」)、explore warm-decision(`explore.py:1081`)、`run_grid` 自己的 warmup。但这治的是**偏差**,不是**方差**。 ### 全仓唯一的正确做法,没有调用方 `agents/kernel/tools/apply_and_bench.py` 是个独立 CLI,方法学完全正确: - `reps: int = 5`(`:743`),每臂 5 次计时重复,外加一次不计时的 warmup - `--seed` 固定,注释写明理由:*"Fixed `--seed` so both arms benchmark the identical random prompt set"*(`:362`) - `_spread()` 算 median / p25 / p75 / stdev(`:552`) - 真的显著性检验——IQR 不重叠(`:876`): ```python significant = (ps_sp["p25"] > bs_sp["p75"]) or (ps_sp["p75"] < bs_sp["p25"]) # None=insufficient reps; False=within noise (flat); True=clears IQR ``` **全仓 grep `significant`,5 处命中全在这个文件自己内部**(初始化、注释、计算、输出 dict、日志行)。零外部消费方。工具自己也写明了:`"gate": "none (... KEEP/REVERT/NEEDS_REVIEW bypassed; policy is the caller's job)"`。调用方 `integrate_patch` / `kernel_stack` 走自己的单次路径,从不读 `apply_and_bench_result.json`。 有人建对了测量装置,然后把决策接到了另一条更差的路上。 另外,orchestrator 路径**从不固定 benchmark 种子**。`_workload_envs.py` 传了 `CONC/ISL/OSL/MAX_MODEL_LEN/TP/RANDOM_RANGE_RATIO`,没有 seed。有 `RANDOM_RANGE_RATIO` 在,同一配置连续两次运行的 prompt 长度分布是重采样的——差的不只是系统噪声,是负载本身。 ### 噪声地板:断言过,从未测量 | 常量 | 值 | 代码里的依据 | |---|---|---| | `explore.DEFAULT_KEEP_THRESHOLD_PCT` | 1.0 | 注释:「grid noise floor」 | | `MULTI_NODE_KEEP_THRESHOLD_FACTOR` | 2.0 | 注释:「Multi-node baseline noise floor is ~2x single-node」 | | `DEFAULT_STACK_STABLE_PCT` | 0.5 | 9 行注释,讲的是内部一致性(复测地板 < KEEP 地板) | | `_grid_runner.SINGLE/MULTI_NODE_DEFAULT_KEEP_THRESHOLD_PCT` | 1.0 / 2.0 | **死常量**,在 `__all__` 里导出,无任何引用 | **仓库里没有任何地方测量过 run-to-run 方差并拿它和 1.0% / 2.0% / 0.5% 比较。**多节点「约 2 倍」这个说法没有数据、没有工单、没有测量脚本。 叠上 §5 的衰减曲线,结论是:如果 1.0% 在第 1 个 cycle 是噪声地板,那么噪声地板并没有变,只是接受线走到了它下面。按代码自己的前提,**中期之后的每一次 KEEP 都和噪声不可区分**。 :::caution[系统自己知道这件事] `agents/robustness/signals/decision_audit.py:182` 有个守卫,对任何 `gain_pct < 1.0` 的 KEEP 报 MEDIUM 级症状,措辞是 *"likely noise-floor"*,建议是: > "raise the executor's keep threshold to >= 1% and require multi-seed confidence for sub-threshold KEEPs" 一个内部审计器建议做多种子置信度;决策路径没有种子也没有重复;而衰减曲线保证这个审计器在第 1 个 cycle 之后几乎每次 KEEP 都会触发。同一文件里的兄弟守卫更锐利:`_g1` 抓「零字节补丁的 KEEP」(没改代码,任何增益都是噪声),`_g3` 抓 `dispatched_count == 0` 或 `|gain| < 0.5%` 的 KEEP(补丁可能根本没执行)。这些直觉都对——只是它们诊断的是一个分不清 0.2% 和 0 的测量过程。 ::: ### 基准锚点是单向棘轮 `resolve_grading_anchor_tput`(`state/shared_state.py:92`)返回 `current_best.tput`,回退到 `baseline_tput`。docstring 的理由是对的:候选是带着 `current_best` 的参数启动的,拿原始 baseline 打分等于「把一个测量值和它从未在上面取过的配置比较」。 但 `current_best.tput` 就是上一个被接受变体的复测值——本身也是单次测量。所以锚点是一列单次测量上的随机游走,**而且只会往上走**(变体只有打赢锚点才被接受)。被接受变体上的测量噪声永久烧进后续所有增益的分母。漂移纠正也是单向的(`explore.py:602`): ```python if not revalidating_stack and live_anchor > base_tput: base_tput = live_anchor # 只在 live > snapshot 时纠正 ``` 会话中不做周期性 baseline 重测。只有 resume 时会比一次,`measured < recorded × 95%` 才报 `current_best_drift` 观测,且**只记日志不改值**。 ### 变体之间的状态隔离 跨变体是干净的,做得很到位:`teardown_lifecycle_server` 在每条退出路径的 `finally` 里跑;`_kill_stale_servers` 扫 `/proc` 找 `VLLM::Worker` / `sglang.srt` 等残留、`killpg(SIGKILL)`、**清 `/dev/shm/{vllm,nccl,cuda,torch,atom}*`**、然后 sleep 2–8s 等 KFD 异步释放显存;每会话换一个临时端口。所以进程内状态(prefix/radix cache、CUDA graph 捕获、KV pool、分配器 arena)不可能跨变体。 **变体内部的状态延续是故意的,这里有个未被处理的偏差。**warmup → 决策 → 复测三轮共用一个持久 server。决策轮跑在一个已经服务过完整 warmup 负载的 server 上,复测轮跑在被 warmup **和**决策轮都预热过的 server 上。没有任何显式 cache flush(grep `flush_cache` 只命中被调优的 flag 本身)。由于复测值**替换头条并成为下一个锚点**,这是系统性向上偏差,不是相互抵消。KV/prefix cache 的复用语义见 [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/)。 跨变体唯一存活的是磁盘编译缓存:aiter JIT 的 `jit/*.so` 是节点共享持久的,`_probe_aiter_jit_cache` 数 `.so` 个数来选冷/热超时。这个不清是对的(清一次要多花 30 分钟),但意味着新会话的第一个变体付了后续变体不付的编译成本。 ### 精度门禁 - 评测:lm-eval **GSM8K**,指标 `exact_match,strict-match`,默认全量 1319 题 - `ACCURACY_THRESHOLD = 0.05` 是**绝对 exact-match 单位的 5 个百分点**,不是 5% 相对。baseline 0.80 → 候选须 ≥ 0.75 - **无 baseline 精度就整个跳过**:`if baseline_accuracy <= 0: return True` 这是全仓唯一一处做了显式噪声-阈值论证的地方,而且论证成立(`docs/reference/environment-variables.md:126`):GSM8K 在 p≈0.85、n=1319 时标准误约 1.0pp,5pp 约等于 5σ,单次运行噪声碰不到。代价是这道门**非常钝**——一个真实的 4 个百分点回退(4σ,毫无疑义是真的)会静默通过,吞吐收益照记。 serving 框架的门禁**只在命中硬编码风险名单时才跑**(`_accuracy_gate.py:336`):5 个 CLI 子串(`--kv-cache-dtype`、`--enforce-eager`、`--compilation-config`、`--attention-backend`、`--decode-attention-backend`)+ 10 个环境变量键。名单外的任何 flag 都不做精度校验。**`--quantization` 不在名单上**——一个量化变体可以只靠吞吐被 KEEP。在一个「整个目的就是让 LLM 提出没人枚举过的 flag」的系统里,用枚举式风险名单是结构性错配。量化对质量的影响边界见[量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/)。 对比之下,scriptable 框架(xDiT)的门禁是**每个变体都跑且 fail-closed**:图像质量门禁缺失/跳过/歧义都判 `accuracy=0.0` → REVERT。serving 路径反而更宽松。 还有个兜底常量值得一提:`DEFAULT_ENABLEMENT_ACCURACY_FLOOR = 0.05`,注释是全文件最好的一条——*"At 0.0 the gate degenerates to `accuracy > 0`, which admits a model that is answering essentially nothing: a real run KEPT a candidate scoring gsm8k=0.00076 (0.08% of a 0.906 baseline) as 'correct'."* 事故后修的,事故记录在案。 另一处仪表做得很好:generation-pathology 探针(`_accuracy_gate.py:71`)区分「模型没吐 EOS、评测被 token 上限截断、得分接近 0」和「模型答了但答错」,触发条件是 ≥128 个采样响应中 ≥75% 撞到 completion 上限。这正是朴素精度门禁会误读的失效模式。 ### 失败分类学 这是系统最强的部分。有效性判据(`benchmark_result.py:881`):`output_throughput > 0` **且** serving 的 `completed_requests > 0`。 12 种 `error_class`:`capability_unsupported`、`yaml_build_error`、`magpie_timeout`、`server_init_dead`、`detokenizer_stall`、`killed_overtime`、`agentx_preflight`、`no_benchmark_workspace`、`benchmark_report_missing`、`benchmark_report_invalid_metric`、`cuda_graph_capture_failed`、`session_time_exhausted`。 「真的更慢」被干净地区分出来了:慢但有效的配置是 `status="succeeded"` + 真实吞吐 + `reason="gain_below_threshold"`;上面每一类失败都是 `status="failed"` + `tput=None` + `gain_pct=None`。**崩溃永远不会伪装成回退,反之亦然。** 几个值得单独点出的细节: - **`killed_overtime` 从不产出决策数字**。会从 `server.log` 的 decode-rate 标记估一个吞吐(丢掉前 25% 样本当 warmup),但落在 `estimated_output_throughput`,`tput` 显式为 `None`,注释写明 *"never enters winner selection or gain math"* - **超时截止时间锚在对的钟上**:warm-decision 模式下软截止是 `baseline_warm_runtime_sec × kill_ratio`(纯客户端时间),从 server-ready 标记开始算;warmup 轮的 `soft_deadline_sec=None`,一次性冷启动不会误触发 - **泄漏产物打捞按 mtime 闸门**:`harvest_leaked_artifacts` 能回收写到 workspace 外的结果,但拒绝任何早于 `subprocess_started_unix - 1.0s` 的文件——上一次运行的结果不可能被当成本次的 - **陈旧评测防护**:`run_eval_disabled` 从子进程**实际消费的**物化 YAML 里回读,不从 params 读,因为评测失败重试会复用 `output_dir`,否则会把上次的 `results*.json` 当本次的 - **有效数字已落盘后的非零退出**降级为 warning,测量保留 - **每次中止都写 `abort_reason.json`**,事后能区分「测过但失败」和「没测」 **缺口**:*有效但退化*的输出不是一个失败类别。一个又快又输出垃圾的配置满足 `output_throughput > 0 and completed_requests > 0`,就是一个有效测量和候选赢家。唯一防线是精度门禁,而 serving 的精度门禁只跑硬编码名单。 ### 并发扫描:验证工件,不是优化器 `kernel/conc_sweep.py` 看着像个经典参数扫描,实际不是: - 固定 8 点阶梯 `DEFAULT_CONCS = [256, 128, 64, 32, 16, 8, 4, 2]`,字面量不是算出来的 - 两臂对照:`baseline`(空配置)vs `optimized`(`current_best`) - **算的是 argmax 加速比**,不是吞吐拐点、也不是延迟预算下的最优点:`best_conc = argmax(optimized_tput / baseline_tput)` - `ttft_mean_ms` / `e2el_mean_ms` **每行都收集了,但没有任何代码读它们做决策**。整个模块没有 SLO 常量 - `best_conc` **从不写回配置**,只进报告 工程实现本身很扎实:boot-retry-descend(在最高 CONC 起不来就降一档重试,失败的高 CONC 记为真实容量失败而不是丢弃)、每点后原子增量落盘 JSON+CSV(硬杀不丢曲线)、每点前查预算。 附带的 decode roofline ceiling(每个并发点的 `T_mem` / `T_cmp` / MBU%)是整个 sweep 里最有原则的部分——它告诉你每个并发点离物理上限多远——也是只进报告。 另有一个可 LLM 提议的完整 workload sweep(`actions/executors/sweep.py`),`[4,16,64] × ["1024:1024","8192:1024","1024:8192"]` 九点全交叉,是全仓唯一算 **Pareto 前沿**(max 吞吐 / min `e2el_mean_ms`)的地方。同样只进报告。批处理与并发的调度语义见[批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/)。 ## 7. 跨运行沉淀:什么真的传下去了 五条声称跨会话传递知识的通道,端到端能走通的只有两条。 | 通道 | 跨运行 | 约束力 | 状态 | |---|---|---|---| | `best_config` → warm-replay | 是 | 强约束(自动应用配置) | **能用**,最强的真实机制 | | `kb/framework_optimization/lessons.jsonl` | 是 | 咨询性 PR 排序 + 一处强约束精度阻断 | **能用** | | `lessons` / `pitfalls` | 持久化正确 | 咨询性(prompt 文本) | 只以裸 JSON blob 到达模型,结构化的 5b/5c 段渲染成 `(none)` | | `what_failed` → `explore_search.rejected` | — | 强约束(永久阻塞) | **死的**,字段在写入时被投影掉 | | `prs_tested` → 补丁重放/阻断 | — | 强约束(补丁过滤) | **死的**,同一原因 | ### 存储键太窄 canonical id 是 7 段(`recipe_snapshot_constants.py:121`): ``` inference:{model}:{hardware}:{framework_name}:{model_type}:{architectures}:{framework_version}:{precision} ``` `framework_version` 是硬键段。`0.4.6` → `0.4.6.post1` 就是另一个目录、另一个 `recipe.json`。级联降级(L1 exact 1.0 → L2 same_arch 0.95 → L3 same_arch_any_version **0.5** → L4 relative **0.3**)救不回来:L2 仍然锁 `framework_version`,所以版本 bump 直接掉到 L3/L4,而 warm-replay 的门槛是 `_DEFAULT_WARM_REPLAY_MIN_CONFIDENCE = 0.7`(`coordinator.py:48`)。**一个 `.post1` 补丁版就静默清零了唯一能用的知识通道。**没有语义化版本距离,没有「同 minor」层级。 硬件方向零迁移:`hardware` 是硬键段,MI300X 和 MI355X 是两个目录,级联的任何一层都不放宽它。这个选择可辩护(为一张卡调的配置在另一张上确实不安全),但意味着跨硬件世代一点知识都不传,连「我们对邻居卡知道点什么」都不提示。 反过来,键在一个要紧的维度上又**太宽**:workload 形状(conc/isl/osl/tp)**不在键里**,只在 `extras` 里做重排提示。所以在 `conc=32, isl=1024` 调出来的配置,会以 tier `exact`、置信度 1.0 返回给 `conc=512, isl=8192` 的运行,并被自动应用。形状冲突门禁 `_shape_conflict` 存在,但 `recipe_kb_t0.py:1211` 在 `warm_tier == "exact"` 时显式绕过它。 ### 写入路径丢数据 `_record_fact_impl`(`loop/writeback.py:898`)是严格两分支: ```python if is_keep and gain_pct is not None and gain_pct > 0: → 追加一条 lesson return # 提前返回 severity = self._pitfall_severity_for(...) # crash/oom/hang,或 gain_pct <= -5.0 if severity is not None: → 追加一条 pitfall # 否则:什么都不写 ``` **中性结果一条都不记。**测出 `gain_pct = 0.0` 或 `+0.3%` 或 `−2%` 的变体——也就是一次调参扫描里的绝大多数——两个分支都不命中,KB 里什么都没有。系统**学不到「这个 knob 在这里没用」**,每次新会话都要重测一遍。 更严重的是字段投影,这条我逐行核实过。`local_store.py:449`: ```python "what_worked": _normalise_str_dicts(what_worked, ("description", "measured_impact")), "what_failed": _normalise_str_dicts(what_failed, ("description", "reason")), ``` 而 `_normalise_str_dicts` 就是 `{k: str(d.get(k) or "") for k in keys}`——**其他键全部丢弃**。写入方(`writeback.py:1521`)发的是 `{"name": ..., "reason": ...}`,连标签都因为键名不匹配(`name` vs `description`)丢了。 读取方(`phases/prelude.py:102`)要的是: ```python args = str(row.get("extra_server_args") or "").strip() envs = row.get("extra_envs") or {} if not args and not envs: continue # 恒真 fp = canonical_fingerprint(args, envs) ``` 写入方没发这两个字段,投影层也不会保留它们,所以这个 `continue` 恒真,**注入 0 行**。而 `explore_search` 是 per-session 状态——这条曾是拒绝记录跨会话传递的唯一路径。 如果它能工作,会是真正的强约束且永久:注入行不带 `outcome` 键,`_is_blocked` 无条件返回 `True`,基于增益的解锁不适用。 `prs_tested` 同一失效模式:`_normalise_prs` 只留 `{repo, number, outcome, notes}`,而下游 `_extract_patches_from_prs_tested` 需要 `patch_content`、`measured_gain_pct`、`applicable_arch`——全没了。所以 `recommended_replay.patches` 和 `blocked_patches` 从本地行永远是空的。 ### 没有去重,`validated_count` 从不写入 `proposals.py:211` 是裸 append,docstring 自己写了 "lesson/pitfall appended without dedup"。 **意图是有的**。`_build_statement`(`writeback.py:1076`)刻意构造了身份稳定的 key,并写明理由:*"MUST exclude volatile fields (e.g. gain_pct) so N sessions merge instead of producing N rows"*。`gain_pct` 作为参数接收然后故意忽略,就是为了让重复观测碰撞。**但没有任何地方去哈希它或比较它。** `validated_count` 全仓 3 处读取(都在 prompt renderer 里),**零处写入**。同一个 lesson 语句跑 3 个会话就是 3 行,无上限、无去重、无 LRU,而这个数组会被整个拷进 prompt。 对照:同一份代码在别处是会去重的——`sessions[]` 按 `session_id`、`kernel_optimizations` 按 `kernel_id`、`explore_search.rejected` 按 `fingerprint`、`emit_fact` 按三元组。recipe 行的 `lessons`/`pitfalls` 只是被漏掉了。 ### 知识图谱:读客户端,没有数据源 `kg_client.py`(1352 行)不是图数据库。默认模式下「图查询」是 gbrain 全文搜索,然后**正则解析 Markdown 的 `## Facts` 围栏**。原生模式(`GBRAIN_KG_NATIVE=1`)把三元组映射到 gbrain 的链接图上,属性通过自由文本 `context` 字段塞 JSON。 10 种谓词有消费方(`REVERTED_ON`、`IMPROVES`、`CONFLICTS_WITH`、`KNOB_IMPROVES`…),但 `emit_fact` 全仓**只有一个生产调用方**:`phases/framework.py:3156` 的 `_emit_kg_decision`,只为**框架补丁决策**写 `IMPROVES` / `REVERTED_ON`,且需要 `GBRAIN_KG_NATIVE` + 可达的原生客户端。 所以 `KNOB_IMPROVES` / `KNOB_REVERTED_ON` **没有写入方**——`graph_guided_knobs` 和 `kg_cross_model` donor 读的是没人生产的边。docstring 点名生产者是 "kb-mirror drivers",那个组件不在 OSS 发布里。而在树内的镜像也救不了:`gbrain_ingest.recipe_to_page` 产出的页面体根本没有 `## Facts` 段。 远端存储(`gbrain_remote_client.py`)严格只读,且默认不可达(需要 `GBRAIN_BASE_URL` + `GBRAIN_TOKEN`,`.env.template` 里两个都是注释掉的占位符)。**OSS 版本里跨运行学习是单机的**,没有共享知识底座。 ### 一处设计正确但从未渲染的功能 `specialist_prompt_builder.py:1441` 的 `_format_version_note`: ```python return f" [from {framework_label}@{lesson_fv}, you're on {current_fv}]" ``` 在每条 lesson / pitfall 上内联标注 `[from sglang@0.4.6, you're on 0.5.1]`,docstring 说 *"the LLM gets the final call"*。这个直觉完全对:标注来源差距、让模型自己判断,比硬丢弃或盲目信任都好。 但它读的 `framework_version` 字段写入方从不发,而且它的两个调用方(`_section_lessons` / `_section_pitfalls`)都读一个 legacy 的 `attrs` 包装形状,当前写入路径不产生 `attrs`,于是两段都提前 return、渲染成 `(none)`。**这个功能从未渲染过。** 顺带一提,`prelude.py` 那边是**兼容两种形状**的(`recipe_attrs = (recipe.get("attrs") or recipe)`),说明这是一次漏掉了 prompt 层的迁移。 ## 8. 工程借鉴清单 ### 值得抄 1. **禁止 agent 自报数字,机器强制**。`FORBIDDEN_PROPOSAL_FIELDS` + 4 条正则扫自由文本,prompt 里明确「the Coordinator measures gain」。这条和「缺失/不可比的数据保持为空、前端不做估算」是同一个原则,但他们做成了机器强制的。 2. **内容寻址的指纹去重**。排序 token + 排序 env pair 的 SHA-1,排除 `name` / `note` 所以改名不影响去重。这个形状是对的——**但要把 workload 形状加进哈希**。 3. **冷热轮分离**。baseline 双跑、explore warm-decision、`run_grid` warmup 三层都真正丢弃冷轮。治偏差有效。 4. **失败分类学**。12 种 `error_class`,「崩溃」和「更慢」严格不混淆;超时不产出决策数字;打捞按 mtime 闸门防跨运行污染;陈旧评测从实际物化的 YAML 回读。这些细节明显是被真实事故打磨出来的(`gsm8k=0.00076` 那条注释、orphaned `FileBaton` 清理、端口复用修复)。 5. **KEEP 后二次复测**。作为**复现性检查**是真实有效的——靠运气赢的变体得再赢一次,能滤掉相当一部分纯噪声赢家。 6. **本地写 / 远端读,且无条件本地兜底**。正确性从不依赖网络,每一次远端失败都有本地答案。 7. **原子写协议**。archive-then-write 加 `flock`、tmp+rename+fsync、单调 version、provenance 里存 `replaced_by`。免费得到完整版本历史。 8. **每次写入的 delta 记账**。`prior_counts` vs `counts` 让「这次写入到底贡献了知识吗」变成可回答的问题——如果有人对它告警,§7 那两处字段丢失第一天就会暴露。 9. **`_donor_is_trustworthy`**。借用他人配置的门禁:有可重放配置 **且** 有正向已验证增益 **且** 架构具体匹配 **且** workload 形状不冲突。docstring 老实写了没有它会怎样:*"Borrowing a champion config on a loose same-arch match empirically produced near-zero or negative replay gains."* 10. **标注来源差距、让模型判断**,而不是硬丢弃或盲信。 ### 不要抄 1. **不要硬投影到固定键组**。`{k: str(d.get(k) or "") for k in keys}` 是两处数据丢失的同一个根因。要么保留未知键(`Recipe.extras` 已经这么做了),要么对意外键**大声失败**。静默清零一个下游必需的字段是最坏的失效模式:没有报错、没有日志、没有测试失败,一个「self-evolving」的系统安静地什么都没进化。 2. **不要让读形状和写形状漂移而没有契约测试**。这几处缺陷是同一类。每一处都有**通过的**单元测试——因为每侧的 fixture 都是按自己那侧期望的形状手写的。**只有过真实 store 的往返测试能抓住这类问题。** 3. **版本当精确键段是知识悬崖**。要么加一个带自己置信度的版本距离层级,要么每个 `.post1` 都丢一次语料。 4. **不要丢弃中性结果**。「knob X 在这里没用」测起来贵、存起来便宜,而且是任何扫描的大多数。只记赢和崩的 KB 会让搜索永远重新推导空结果。 5. **接受阈值不能衰减到自己声称的噪声地板之下**。`KEEP_THRESHOLD_FLOOR_PCT` 应该是 `max(0.1, k·σ)`,而 σ 得先测出来。 6. **不要发一个读客户端却没有能写的语料**。KG 的 1352 行在 OSS 版本里是没有数据源的架构。 7. **不要建只写通道**。`kernel_optimizations` 和整个 `Attempt` schema(`predicted_delta` / `measured_metrics` / `fitness`,一套完整的演化搜索记录)都没有写入方或读取方。 8. **`confidence` 要么算出来要么删掉**。永久钉在 `0.85` 的字段会诱导下游把它当证据强度用。 ### 如果要让它变成真的优化器 最高杠杆的改动不是换个更好的提议者,而是三件事:把 workload 形状加进接受台账的键;在已接受集合上做交互搜索(`synergy_attempted` 目前是死字段);把并发扫描的 Pareto 前沿和 MBU% 在一条明确的延迟 SLO 下接回 `current_best`。 统计学侧最小可行改动:会话开始时把同一配置连测 10 次(双跑守卫已经把 server 起好并持有,边际成本只是客户端轮),把 σ 写进 `state.json`,让所有阈值从 σ 推导,然后把 `apply_and_bench.py` 已经写好的 5 次重复 + 固定种子 + median + IQR 检验接到决策路径上。那个 `significant` 字段现在没人读——要么消费它,要么删掉它,因为发一个没人读的正确显著性检验比没有更糟,它从外面看起来像严谨。 :::caution[怎么读 Hyperloom 报出的累计收益] 一次长运行报出的累计收益,应该读作「一条单向棘轮在单次测量上累加、且接受线衰减到系统自称噪声地板之下」的上界,**不是测量到的加速比**。要问的数字不是那个 gain,而是拿最终 `current_best` 配置和原始 baseline 配置在同一台机器上各自重复测几次的干净对比。 ::: ## 9. 复核边界 - 本文基于 2026-08-04 的主干快照静态阅读,**没有实际运行过 Hyperloom**。「死代码」判定基于全仓 grep 加调用图追溯;衰减曲线、`significant` 孤儿、KB 字段投影三条逐行复核过。 - 项目是 v1.0.0a2(alpha),2026-03-27 建仓,README 挂着 beta 问卷。这里指出的缺陷有相当一部分是 alpha 阶段的正常状态,不代表最终形态。 - §6 的方法学批评针对**决策路径**。`apply_and_bench.py` 证明团队里有人知道正确做法,问题是接线。 - README 与实现不一致的地方("tree-based cognition layer"、"self-evolving"、`specialist.yaml` 的 `max_turns: 12` 和 `allowed_tools` 列表)在正文对应位置已标注。 ## 相关阅读 - [Agentic Infra:LLM 推理性能优化与 GPU 利用率提升](https://weigao.cc/ai-systems/llm-inference/agentic-infra-inference-optimization/) — 观测 → 归因 → 最小干预 → 验证的方法论框架 - [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) — Hyperloom roofline 饱和判据(`within_roofline_pct ≥ 95%`)背后的模型 - [批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) — `--max-num-seqs` / `--max-num-batched-tokens` / chunked prefill 这些 knob 的语义 - [推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) — sglang / vLLM 的实现差异,解释了为什么跨框架 knob 翻译做不成通用层 - [量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/) — 为什么 `--quantization` 不进精度门禁名单是个问题 - [推理 Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) — CUDA Graph、Kernel Fusion 这些 knob 改动的底层机制 - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 与 Hyperloom 纯实测路线相对的另一条:先建模再校准 --- ## vLLM Async Scheduling:三态配置、投机解码与状态提交 > Source: https://weigao.cc/ai-systems/llm-inference/vllm-async-scheduling-speculative-decoding/ > Date: 2026-08-05 > Tags: llm-inference, vllm, decode, kv-cache, trace Async scheduling 和 speculative decoding 都在“提前”:前者让 CPU 在 GPU 返回前排下一轮,后者让 Drafter 在 Target 确认前猜未来 token。两者组合后,系统必须同时回答:**哪些 token 已确认,哪些只是占位,哪些 KV 已经计算但还不能成为可复用历史?** 这篇沿 vLLM 源码回答三个具体问题: 1. `async_scheduling=False / True / None` 应该是什么契约; 2. EAGLE 与 MTP 的 hidden、embedding、LM head、draft KV 和 target KV 到底是什么关系; 3. 为什么 async 已经开启,Trace 里仍可能看不到 scheduler 与 GPU forward 重叠。 :::important[30 秒复习] - **一句话**:Async Scheduler 用 placeholder 代表尚未回来的 token,再按 accepted prefix 修正逻辑前沿;EAGLE/MTP 都走 draft → target verify,不应按“旁路是否污染主 KV”二分。 - **三个判断**:先区分 requested config 与 resolved runtime;再区分 target token、placeholder、draft token 与 computed frontier;最后用 Trace 验证 schedule 是否真的覆盖 GPU forward。 - **核心模型**:`confirmed frontier = num_computed_tokens - num_output_placeholders`。Draft 被拒绝时,回退的是逻辑前沿;线性位置的旧 KV 随后会被正确计算覆盖。 - **边界**:本文锁定社区 `main@beca88e59`;框架能力、支持矩阵和默认值会演进,生产结论必须绑定版本与 backend。 ::: ## 1. 三态配置不是三个布尔值 一个性能开关若允许 `None`,就同时承担了“用户意图”和“框架决策”两层语义: | 请求值 | 含义 | 不兼容时 | |---|---|---| | `False` | 用户明确关闭 | 保持关闭 | | `True` | 用户明确要求开启 | 启动失败并返回原因 | | `None` | 自动决策 | 支持则开启;不支持则关闭并记录原因 | 最容易出错的写法,是把用户传入值原地改掉: ```python if async_scheduling and incompatible: async_scheduling = False if async_scheduling: # 这里已经不知道用户原来是否传了 True ... ``` 更稳的实现会分开: ```text requested: False | True | None resolved: False | True reason: explicit_disabled | compatible_default | unsupported_backend | ... ``` 社区当前源码把默认值设为 `None`,显式 `True` 遇到不兼容组合会 hard fail,`None` 才允许自动降级: - [`SchedulerConfig.async_scheduling`](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/config/scheduler.py#L148-L150) - [显式开启与自动模式的兼容校验](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/config/vllm.py#L1070-L1161) 这不只是 API 美观问题。性能特性若 silent fallback,服务可以正常启动,但使用者误以为优化已生效,随后把“配置未开启”误诊成“优化没有收益”。 ## 2. Async Scheduler 提前了哪一步 同步执行每轮都等待结果: ```text schedule(N) → execute/sample(N) → update(N) → schedule(N+1) ``` Async Scheduler 希望把下一轮 CPU 工作前移: ```text GPU: execute + sample(N) ──────────────┐ CPU: schedule + prepare(N+1) ─┼→ reconcile(N) └→ execute(N+1) ``` CPU 此时不知道下一 token 的值,只知道“本轮至多会产生多少 token”。因此它先记占位数量、分配执行资源,输出回来后再兑现。 ### 五本不能混写的账 | 状态 | 表示什么 | |---|---| | `output_token_ids` | 已返回并提交到请求的真实 token | | `num_output_placeholders` | 已排程、尚未由 GPU 输出兑现的位置 | | `spec_token_ids` | 下一轮交给 Target 验证的 draft token;在途时可先填 `-1` | | `num_computed_tokens` | Scheduler 认为已经处理到的逻辑前沿,可能包含在途工作 | | KV block / slot | 已分配、甚至已被 Kernel 写入的物理状态 | vLLM 的 `AsyncScheduler._update_after_schedule()` 会预加 sampled-token 与 draft-token placeholder;输出返回后再减去已兑现的数量,并只把 confirmed frontier 之前的 block 作为可缓存状态: - [placeholder 预分配与兑现](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/v1/core/sched/async_scheduler.py#L12-L69) 可以把关键不变量记成: ```text confirmed frontier = num_computed_tokens - num_output_placeholders ``` `num_computed_tokens` 不是“用户已经看到几个 token”,placeholder 也不是假 token 值;它们共同描述 CPU 为在途 GPU 工作预留到了哪里。 ## 3. Speculative Decoding 又增加了一层不确定性 假设当前 confirmed context 是 `A B C`,Drafter 提出: ```text D E F ``` Target 批量验证后可能得到: ```text D accepted E rejected X correction token F invalid after first rejection ``` 下一轮真实起点是 `A B C D X`,但 CPU 想提前调度时还不知道接受了几个 draft token。accepted count 会改变: - position ids 与 query length; - block table、slot mapping 与可复用 KV 边界; - repetition penalty、bad words 等 token 历史; - structured output FSM; - stop / max-token 判断; - Mamba、GDN 等递推状态的有效位置。 所以 async + spec decode 的根冲突不是“会不会丢 token”,而是: > CPU 排下一轮所需的 commit frontier,仍在 GPU 的 rejection sampling 结果里。 输出回来后,Scheduler 根据 `num_draft_tokens - num_accepted` 得到 rejected 数量,同时回退 computed frontier 与 placeholders: - [rejection 后的 frontier 修正](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/v1/core/sched/scheduler.py#L1767-L1789) ## 4. Target KV 会不会被“污染” Target 验证候选时,确实会为候选位置执行 forward 并写 KV。这里 EAGLE 与 MTP 没有本质差异。 首次拒绝后的那些 KV 不能作为正确历史继续使用,但线性 accepted-prefix 场景通常不需要清空整段显存: ```text 回退逻辑 frontier → 下一轮从 accepted prefix 的后一位置继续 → 正确 token 的 KV 覆盖相同逻辑位置 ``` 正确性取决于 position、slot mapping、block 生命周期和 confirmed frontier 一致。树状 speculative decoding 的 accepted path 可能来自非连续槽位,才需要额外整理或复制接受分支。 因此,“有没有计算过候选 KV”不是判断主路径是否干净的标准。更有效的问题是: 1. 这段状态属于 target verifier 还是 drafter? 2. Scheduler 把哪一个位置当成 confirmed frontier? 3. rejected 位置会被忽略、覆盖,还是需要搬运? ## 5. EAGLE 与 MTP 的真实差别 在当前 vLLM 中,常见 EAGLE 与 MTP 都会进入 `use_eagle()` 分支并构造 `EagleProposer`: - [GPUModelRunner 的 proposer 选择](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/v1/worker/gpu_model_runner.py#L627-L657) 共同 serving 协议是: ```text Target hidden → Drafter 提出候选 → Target 批量验证 → RejectionSampler 保留 accepted prefix / 产生 correction → Scheduler commit + rollback logical frontier ``` 差别主要在 Drafter 从哪里来、复用什么: | 维度 | EAGLE | MTP | |---|---|---| | 训练产物 | 通常是与 Target 匹配的独立 EAGLE checkpoint/head | 通常是随 Target checkpoint 训练、交付的额外 MTP module/layer | | Hidden 输入 | 读取 Target feature;EAGLE3 可组合多层 aux hidden | 读取 Target hidden;某些架构复用 pre-final mixer 或多流 hidden | | Embedding | checkpoint 可自带;缺失或完全相同时可共享 Target embedding | vLLM 默认共享 Target embedding | | LM head | checkpoint 可自带;缺失或相同时可共享 | vLLM 默认共享 Target LM head | | Draft 依赖 | 轻量模型/head 自回归或树状生成 | 多个 MTP step 可连续调用,后一步依赖前一步候选与 hidden | | Draft cache | Draft attention layer 有自己的 KV/state | MTP draft layer同样有自己的 KV/state;特定架构可能还有递推 cache | | Target verify | 为候选位置计算 Target state | 相同 | | Scheduler commit | 按 accepted prefix 统一结算 | 相同 | 源码直接展示了参数共享策略: - [Embedding:EAGLE 条件共享,MTP 默认共享](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/v1/spec_decode/llm_base_proposer.py#L1425-L1489) - [LM head:EAGLE 条件共享,MTP 默认共享](https://github.com/vllm-project/vllm/blob/beca88e59ea75a7aa1af72a5ae50188fa91d4e3d/vllm/v1/spec_decode/llm_base_proposer.py#L1525-L1567) 一句更准确的话是: > EAGLE 是独立训练的 feature drafter,MTP 是模型内生、随 Target 交付的 drafter;二者都属于 speculative branch,也都要由 Target 验证。 ## 6. 为什么 MTP 的工程实现仍可能更难 “两者共用 serving 协议”不代表实现成本完全相同。MTP 的额外复杂度通常来自: - 多个 draft step 的 hidden/KV 依赖链; - 与 Target 共享 embedding、LM head 或中间 buffer; - MTP module 和 Target 架构、checkpoint layout 绑定更紧; - GDN/Mamba/short-conv/HC 等模型还维护 attention KV 之外的递推状态; - 多 step MTP 的 metadata、padding、CUDA Graph shape 更复杂。 这些约束支持的结论是“需要逐模型和 backend 验证”,而不是“MTP 不能 async”。社区当前配置已经把 EAGLE/MTP 列入 async scheduling 支持范围。 ## 7. 一次真实优化分析应该怎样写 只看到一个 7ms 的 `cudaEventSynchronize`,很容易直接写成“去掉同步可省 7ms”。这通常过早。 一条更完整的证据链是: | 层次 | 要回答什么 | 示例 | |---|---|---| | 事实 | Trace 里实际发生了什么 | async 类已启用,但 schedule 没进入当前 GPU execute 窗口 | | 归因 | 哪个依赖阻止了提前调度 | CPU 等待 accepted count,才能修正下一轮 position / slot | | 排除 | 等待本身是否就是 GPU 空泡 | 同步期间 GPU 可能仍在执行上一轮尾部,因此不能把等待时长直接当收益 | | 优化目标 | 真正想拿回什么 | 恢复 scheduler/prepare 与 GPU forward 的重叠,而非删除一个 API 名 | | 验证 | 如何证明收益 | 同一 Case 对比 schedule-overlap、step wall time、TPOT、吞吐和输出一致性 | ### 推荐的 A/B 合同 固定: ```yaml model: checkpoint + revision runtime: vLLM commit + backend hardware: GPU + topology workload: ISL/OSL distribution + concurrency speculation: method + gamma + sampling ``` 对比: ```text A: async_scheduling=False B: async_scheduling=True ``` 至少记录: - 每轮 proposed / accepted / rejected token; - `schedule` 与 GPU execute 的重叠比例; - accepted-count copy / event wait 的 CPU self-time 和 GPU overlap; - step wall time、TTFT、TPOT、aggregate throughput; - placeholder 最终归零、输出一致性、preemption / structured-output 错误。 如果再改 accepted-count GPU correction,应增加第三组并单独验证 batch、logprobs、tree spec decode 与 stateful model 边界。 ## 8. 面试时怎么讲这个案例 不要从“我发现一个 if 写错了”开始。更有区分度的结构是: 1. **契约**:一个三态性能配置把显式意图与自动策略混在同一可变字段里,造成 silent fallback。 2. **机制**:Async Scheduler 用 placeholder 表示在途 token;spec decode 又让 accepted count 到 GPU 返回后才确定。 3. **源码纠偏**:沿 `Config → AsyncScheduler → Scheduler update → GPUModelRunner → EagleProposer` 追踪后,发现 EAGLE/MTP 共用 proposer/verify/accounting 主链,推翻了“一个旁路、一个污染主 KV”的简单解释。 4. **Trace 证据**:async 类名出现不等于流水有效;要检查 schedule 是否真的覆盖 GPU forward,以及 accepted-count sync 的暴露部分。 5. **决策**:先修配置契约和最终状态回读,再把优化目标定为 GPU-side correction / 延迟 reconciliation,并用固定 Case 做 A/B。 6. **边界**:源码证明机制,单条 Trace 证明一个 Case;没有受控实验前不承诺收益倍数。 ### 90 秒版本 > 我遇到过一个 async scheduling 三态配置问题:显式 True 在某些 guard 里会被静默改成 False,而自动模式又比实际 capability 更保守。继续追源码后,我发现问题不只是配置。Async Scheduler 会提前为下一轮 token 和 KV 分配 placeholder,但 speculative decoding 的 accepted count 要到 GPU rejection sampling 后才知道,所以 computed frontier 必须回退。EAGLE 和 MTP 在 vLLM 里都走 EagleProposer 和 Target verify,差别主要是 Drafter 的来源与参数/状态复用,并不是一个碰主 KV、一个不碰。Trace 里即使 AsyncScheduler 已启用,也要验证 schedule 是否真的覆盖 GPU forward;一个 event wait 如果期间 GPU 仍在忙,不能把整个等待时长算成可回收收益。最后我的方案是把 requested/resolved config 分开、增加结构化 reason,再通过 GPU-side correction 或延迟 reconciliation 恢复重叠,并用相同 workload 做正确性和 TPOT/吞吐 A/B。 这段叙述同时展示了配置设计、Scheduler 状态机、KV 生命周期、源码追踪、Trace 归因和实验边界,比罗列优化名词更有说服力。 ## 9. 读源码的最短路径 | 问题 | 入口 | |---|---| | 三态默认值是什么 | `vllm/config/scheduler.py` | | 显式与自动模式如何解析 | `vllm/config/vllm.py` | | placeholder 何时增加/兑现 | `vllm/v1/core/sched/async_scheduler.py` | | rejected token 如何回退 | `vllm/v1/core/sched/scheduler.py` | | MTP/EAGLE 如何选择 proposer | `vllm/v1/worker/gpu_model_runner.py` | | hidden、embedding、LM head 如何进入 Drafter | `vllm/v1/spec_decode/llm_base_proposer.py` | 先沿状态写入者阅读,再看类名和注释。真正能串起系统的是 `num_output_placeholders`、`num_computed_tokens`、`spec_token_ids`、block/slot frontier 如何一起变化。 ## 10. 证据边界 - 当前社区源码证明 EAGLE/MTP 已进入 async compatibility matrix,不证明任意模型/backend 组合都无 bug。 - Config resolved 为 `True` 只证明选择了 async 路径,不证明 CPU/GPU 已获得有效重叠。 - Trace 中某个同步 span 很长,不等于整段时间都是 GPU idle 或可回收收益。 - MTP/EAGLE 的 accepted length、额外验证成本与容量影响需要真实 workload 指标。 - 生产优化案例公开时应移除服务身份、私有 commit、Trace 文件名和不可公开参数,但保留实验合同、观测与推理过程。 ## 相关页面 - [投机解码基础](https://weigao.cc/ai-systems/llm-inference/05-speculative-decoding/) — Draft/Verify、接受前缀、分布等价与收益模型 - [DSpark 与 MTP](https://weigao.cc/ai-systems/llm-inference/dspark-vs-mtp/) — Drafter 结构、置信度和硬件感知验证预算 - [批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) — Continuous Batching 与 Scheduler 的基础合同 - [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) — block、slot、逻辑序列与物理状态 - [Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) — 同步、launch、CUDA Graph 与端到端占比 - [Profiling → Simulation 证据链](https://weigao.cc/ai-systems/profiling/profiling-to-simulation-evidence-chain/) — 事实、归因、模型和决策如何分层 --- ## Kimi K3:架构、训练与推理系统研究 > Source: https://weigao.cc/ai-systems/llm-inference/kimi-k3-architecture-training-inference/ > Date: 2026-08-04 > Tags: llm-inference, attention, moe, distributed-training, quantization Kimi K3 的看点是同时扩大三条信息通路:KDA 沿 token 序列压缩并更新历史,AttnRes 跨深度选择前层表示,Stable LatentMoE 在宽度方向调用大量专家。理解 K3 最直接的路径,是跟随一个 token 走完一次前向。 :::important[30 秒复习] - **一句话**:K3 用 69 层固定状态 KDA 承担大部分长序列混合,用 24 层 Gated MLA 周期性恢复全局逐 token 访问,再用 AttnRes 和 Stable LatentMoE 扩展深度与宽度信息流。 - **三个判断**:KDA state 是固定大小的递推矩阵,与 KV Cache 属于两类对象;层序为 `KDA×3 → MLA×1` 重复 23 次、末尾再补一层 MLA;MXFP4 的作用范围限于 routed expert 权重。 - **核心模型**:2.78T total / 104.2B activated,93 层,hidden size 7168,96 heads,896 routed experts、top-16、2 shared experts,最大上下文 1,048,576。 - **边界**:本文基于 2026-07-27 技术报告、官方配置和参考实现;生产集群的 TP/PP/EP 规模、实际 kernel 时延与完整容量数据仍属内部信息。 ::: ![Kimi K3 一次前向的架构主线](https://weigao.cc/docs/ai-systems/llm-inference/images/kimi-k3-forward-architecture.svg) ## 1. 先冻结真实配置 以下参数同时得到 K3 技术报告 Table 1 与发布版 `config.json` 支持。报告中的圆整说法是 2.8T / 104B,精确表格口径是 2.78T / 104.2B。 | 维度 | Kimi K3 | 解释 | | --------------------- | ----------: | ------------------------------------ | | 总参数 | 2.78T | 大部分位于 routed experts | | 每 token 激活参数 | 104.2B | 每 token 只走这条计算路径 | | Transformer 层数 | 93 | 69 KDA + 24 Gated MLA | | Hidden size | 7168 | 与 Kimi K2 相同 | | Attention heads | 96 | KDA head dim 为 128 | | Routed experts | 896 | 每层专家池 | | Active routed experts | 16/token | 稀疏度为 896/16 = 56 | | Shared experts | 2 | 每 token 都执行 | | LatentMoE width | 3584 | full width 的 0.5× | | Expert intermediate | 3072 | routed expert 的 GLU 中间维度 | | Dense FFN layers | 1 | 仅第 1 层为 dense,其余层走 MoE | | Vocabulary | 163,840 | 配置中的 `vocab_size` | | 最大上下文 | 1,048,576 | 即 1M token | | ViT | 401M / 27 层 | patch size 14,12 heads | | AttnRes block size | 12 层 | 8 个 layer blocks,另含 embedding source | ### 1.1 真实层序 发布配置明确列出全局注意力层: ```text 4, 8, 12, ..., 88, 92, 93 ``` 因此前 92 层是 23 个四层混合组: ```text Layer 1 KDA Layer 2 KDA Layer 3 KDA Layer 4 Gated MLA ... Layer 89 KDA Layer 90 KDA Layer 91 KDA Layer 92 Gated MLA Layer 93 Gated MLA # 额外的最终全局层 ``` 这恰好得到 `23 × 3 = 69` 个 KDA 和 `23 + 1 = 24` 个 MLA。最后再放一个 MLA,是为了保证 backbone 的最终层一定执行全局注意力。 :::caution[两种 block 的所指] KDA/MLA 的“四层混合组”描述 token mixing 的排列;AttnRes 的“12 层 block”描述 depth mixing 的缓存和聚合边界。前者作用于 token 维,后者作用于深度维。 ::: ## 2. 沿一次前向过程看 K3 输入首先变成共享 hidden states,然后依次通过 93 个 attention + FFN/MoE 层: ```text 文本 token ───────────────┐ ├─> shared embeddings 图像/视频 -> MoonViT-V2 -> MLP projector ┘ -> AttnRes 选择本层输入 -> RMSNorm -> KDA 或 Gated MLA -> 更新当前 AttnRes block 的 prefix sum -> AttnRes 再选择 MoE 输入 -> RMSNorm -> Dense FFN(仅首层)或 Stable LatentMoE -> 更新 block prefix sum -> 下一层 -> final AttnRes -> RMSNorm -> LM Head ``` 标准 PreNorm Transformer 只有单一 residual stream;K3 的 `hidden_states` 更像当前 12 层 block 内的部分和,同时保存此前 block 的表示。每个 attention 和 MoE 子层开始前,AttnRes 都会重新决定“从哪些深度取信息”。 ## 3. KDA:把历史写进固定状态 ### 3.1 状态更新公式 对单个 head,令 $q_t,k_t\in\mathbb{R}^{d_k}$,$v_t\in\mathbb{R}^{d_v}$,状态 $S_t\in\mathbb{R}^{d_k\times d_v}$。KDA 的一步更新为: $$ S_t= \left(I-\beta_t k_tk_t^\top\right) \operatorname{Diag}(\alpha_t)S_{t-1} +\beta_t k_tv_t^\top, \qquad \tilde{o}_t=S_t^\top q_t. $$ 可以把它拆成三个动作: 1. `Diag(α_t)`:每个 key channel 独立遗忘旧状态; 2. `I - β_t k_t k_tᵀ`:先擦除旧状态在当前 key 方向上的内容; 3. `β_t k_t v_tᵀ`:再把当前 value 写到该 key 方向。 等价的直觉形式是: $$ S_t=D_tS_{t-1}+\beta_tk_t\left(v_t^\top-k_t^\top D_tS_{t-1}\right), \qquad D_t=\operatorname{Diag}(\alpha_t). $$ 括号里的量是“希望写入的 value”和“当前状态已经读出的 value”之间的误差。这就是 delta rule:按误差量修正一份有限状态。 ### 3.2 q、k、v、α、β 从哪里来 官方参考实现的主路径是: ```text x ├─ Wq -> ShortConv(k=4) -> Swish -> L2Norm -> q ├─ Wk -> ShortConv(k=4) -> Swish -> L2Norm -> k ├─ Wv -> ShortConv(k=4) -> Swish -> v ├─ low-rank decay projection -> α └─ Wβ -> Sigmoid -> β ``` ShortConv 给 KDA 一个很短的局部时序感受野;递推状态负责压缩更长历史。$\beta_t$ 控制当前 token 的写强度,$\alpha_t$ 则是细到每个 key channel 的保留率。 K3 把 log-decay 改为有下界的参数化: $$ g_t=g_{\min}\operatorname{Sigmoid}(e^Az_t), \qquad \alpha_t=\exp(g_t), \qquad g_{\min}=-5. $$ 于是每步 retention 都满足 $\alpha_{t,j}>e^{-5}$。这项下界同时带来 kernel 收益:16-token tile 的累计 log-decay 被限制在 $(-80,0)$,倒数缩放落在 BF16 动态范围内,对角 tile 因此可以直接用 Tensor Core dense matmul 一次算完。 ### 3.3 K3 相比 Kimi Linear 的另一项变化 KDA 输出先做 head-wise RMSNorm,再乘输入相关的 full-rank gate: $$ y_t=W_o\left[\operatorname{Sigmoid}(W_gx_t)\odot \operatorname{RMSNorm}(\tilde{o}_t)\right]. $$ Kimi Linear 使用低秩输出门;K3 改成 full-rank,让每个 token 独立调制读出的各个 channel。 ### 3.4 KDA state 与 KV Cache 的本质区别 | 对象 | 规模随上下文 | 表示什么 | Decode 动作 | |---|---|---|---| | Full/MLA KV Cache | 线性增长 $O(S)$ | 每个历史 token 的可检索表示 | 当前 query 读取历史 entries | | KDA recurrent state | 恒定 $O(1)$ | 全部历史压缩后的矩阵状态 | 原地更新一个固定矩阵 | | ShortConv state | 恒定 $O(1)$ | 最近几个投影 token | 更新长度 4 的局部窗口 | K3 配置为 96 heads、$d_k=d_v=128$。单个 KDA 层、单个请求的主 recurrent state 逻辑元素数为: $$ 96\times128\times128=1{,}572{,}864. $$ 若只按 BF16 payload 粗算约为 3 MiB/层;69 层约 207 MiB/请求,此外还有 ShortConv state、对齐、分片、checkpoint 和运行时副本。这是根据公开 shape 的**逻辑量推导**,生产 resident memory 需以实测为准。 ## 4. 为什么仍然需要 Gated MLA KDA 的优势与代价同源:状态大小恒定,代价是任意长历史都被压缩进一个有限矩阵,历史 token 因此失去 softmax attention 那种独立寻址入口。 K3 于是保留 24 层 Gated MLA,让它周期性承担全局内容检索: | KDA | Gated MLA | |---|---| | 固定 recurrent state | 随 token 增长的 latent KV | | 强于顺序、局部与 recency mixing | 强于跨长距离的全局内容寻址 | | Decode 成本与历史长度解耦 | 读取全局历史,缓存表示已压缩 | | NoPE 下隐式携带位置线索 | K3 中同样使用 NoPE,位置敏感性由 KDA 层提供 | MLA 先把 hidden state 压缩为 latent: $$ c_t=W_cx_t, $$ 再上投影为各 attention heads 的 content key/value。K3 的 MLA 走 NoPE,在每 3 个 KDA 层之后读取一次全局内容。扩展到 1M context 时因此省去 RoPE base 调整与 YaRN。 MLA 输出也使用 full-rank channel gate: $$ y_t=W_o\left[\operatorname{Sigmoid}(W_gx_t)\odot\tilde{o}_t\right]. $$ 所以“Gated MLA”的 gate 作用在 attention 输出的 channel 维度上,由当前输入决定开合。 ### 4.1 与 Full Attention、MLA、DSA 的对照 | 机制 | 历史表示 | 每步访问 | 长上下文主要代价 | |---|---|---|---| | Full Attention | 完整逐 token K/V | 全历史 | KV 容量与 dense attention | | MLA | 逐 token latent KV | 全历史 | cache 变小,访问长度仍为 $S$ | | DSA | 逐 token latent KV + indexer | 选出的 top-k 历史 | 索引扫描、稀疏 gather 与 cache | | KDA | 固定 recurrent state | 当前 state | 状态更新串行依赖与大 state 流量 | | K3 Hybrid | 69 层 KDA + 24 层 MLA | 多数层读 state,周期性读全历史 | 两类 cache、两套 kernel 与一致性管理 | 表中 DSA 指 DeepSeek Sparse Attention,此处仅作“稀疏选择历史”的对照,属于 K3 之外的方案。DSA 削减主 attention 参与计算的 token 数,代价留在 indexer 与逐 token cache;KDA 则直接改变历史的表示形式。 ## 5. AttnRes:沿网络深度做 Attention 普通 residual connection 把所有前层信息累加进一个 hidden state。层数增加后,早期表示容易被连续归一化和残差累加稀释。AttnRes 把深度看成另一条可检索序列。 ### 5.1 Full AttnRes 第 $l$ 层使用一个可学习 pseudo-query $w_l$,对 embedding 与所有前层输出计算 softmax 权重: $$ \alpha_{i\to l}= \frac{\exp\left(w_l^\top\operatorname{RMSNorm}(v_i)\right)} {\sum_{j RMSNorm keys -> 本层 pseudo-query 打分 -> softmax over depth -> 加权合成为本子层输入 ``` 93 层形成 8 个 layer blocks,其中最后一个为 9 层;再把 embedding 作为第 0 个 source,共得到 9 个 block-level sources。内存与通信从 $O(Ld)$ 降为 $O(Nd)$。 ## 6. Stable LatentMoE:沿宽度扩展专家空间 ### 6.1 一次 routed path 对每个 token $x\in\mathbb{R}^{7168}$: ```text x ├─ two shared experts at full width ----------------------┐ └─ router: Sigmoid(Wr x) │ -> bias-corrected top-16 of 896 │ -> W_down: 7168 -> 3584 │ -> dispatch to 16 latent experts │ -> weighted combine -> RMSNorm -> W_up: 3584 -> 7168 │ ├─> add shared path ----------------------------------------------┘ ``` LatentMoE 的关键是:router 仍看 full-width hidden state,routed expert 则只处理 3584 维 latent。EP payload 与每个专家的权重流量都按 latent 宽度计费,active expert 数因此可以放到 16。 ### 6.2 三个“Stable”控制点 1. **Normalized LatentMoE**:16 个 routed expert 聚合后先 RMSNorm,再做 `W_up`,压住各种路由组合造成的尺度漂移。 2. **SiTU-GLU**:对 gate branch 和 up branch 分别 soft-cap,K3 使用 $\beta_1=4$、$\beta_2=25$,把 SwiGLU 两个无界分量的乘积约束在有限范围。 3. **Quantile Balancing**:用专家 bias 调整下一批 token 的 top-k 选择,使每个专家接近目标负载;bias 只影响选择,mixture weight 仍取原始 router 分数,推理时冻结。 SiTU-GLU 为: $$ \left[\beta_1\tanh\left(\frac{W_gx}{\beta_1}\right) \odot\operatorname{Sigmoid}(W_gx)\right] \odot \left[\beta_2\tanh\left(\frac{W_ux}{\beta_2}\right)\right]. $$ Quantile Balancing 用直方图近似全局 quantile:每个 expert 维护一份 histogram,各 rank 的 bin counts 做一次 all-reduce 即可,省去收集全部 margin。 ## 7. 原生视觉路径 K3 从预训练第一步就联合优化文本 backbone 与 MoonViT-V2,视觉编码器与语言模型同期成长。 - MoonViT-V2:27 层、约 401M 参数、12 heads、patch size 14; - 从随机初始化开始直接用 next-token prediction 训练; - 图像和视频共享参数,attention 分为空间与时间两部分; - 投影前做 `2×2 pixel shuffle`,视觉 token 数减少 4 倍; - 支持最高 3584×3584 输入; - 轻量 MLP projector 将视觉特征映射到 LLM embedding space。 报告的消融显示,从头训练的 MoonViT-V2 相比 SigLIP 初始化的 MoonViT-3D 具有更低、更少尖峰的 gradient norm,同时视觉评测相当。该结论的适用范围限于 K3 的训练方案。 ## 8. 架构是怎样确定的 公开证据可以支持三层结论: 1. **前身实验**:Kimi Linear 在相同训练 recipe 下比较 KDA–MLA hybrid 与 full MLA,报告 hybrid 在短上下文、长上下文和 RL scaling 中都有质量优势,并在 1M context 展示更低 KV 占用和更高 decode throughput。 2. **K3 scaling law**:团队为新架构重新搜索 batch size、learning rate、tokens-per-parameter 和 model shape;cosine 与 WSD 各自寻优后再比较。 3. **最终联合收益**:K3 技术报告披露的口径是“架构、数据与训练 recipe 合计约 2.5× scaling efficiency”,模块级归因留给后续消融。 所以合理的表述是:K3 建立在小模型与缩放实验之上,2.5× 属于整体收益。 ## 9. 训练系统 ### 9.1 预训练 recipe - 文本、图像和视频从训练开始就在同一 next-token objective 下联合优化; - matrix parameters 使用 Per-Head Muon,attention 的 Q/K/V momentum 按 head 分块正交化; - cosine learning-rate decay,1% linear warmup,weight decay 0.1; - 预训练先使用 8K context,随后扩到 64K; - long-context cooldown 再按 256K → 1M 扩展。 K3 全 backbone 使用 NoPE,位置信息由 KDA 的卷积、递推门控和 decay 隐式提供,扩到 1M 时因此省去位置编码的重新标定。训练数据侧通过长文档/视频清洗、去重、感知哈希、质量过滤和合成长程依赖任务,把长程依赖压进训练信号。 ### 9.2 3T MoE 并行与 MoonEP 报告公开的训练并行组合包括: ```text PP + virtual pipeline stages + EP + ZeRO-1 data parallelism + Pipeline ZeRO-2 gradient sharding + Context Parallelism ``` 具体并行度属于内部配置。MoE 侧使用 MoonEP:根据当前 micro-batch 路由结果动态复制少量热点 experts,使每个 EP rank 精确接收相同的 `sequence × top-k` token 数。平衡后每层 shape 静态已知,host 逐层读取 expert token count 的同步随之省去。 MoonEP 还将 token 直接送到远端 expert-grouped buffer,路径上省去中间 copy;routed expert GEMM 使用 workload-aware 调度,共享 expert 放到独立 stream 与其他工作重叠。 ### 9.3 KDA Context Parallelism 普通 linear attention 的 additive state 可以直接对 rank 局部结果求前缀和;KDA 的 token-dependent transition 会改变传入 state,各 rank 从零算出的状态因此需要先乘上累计 transition 再组合。 KDA Context Parallelism(KCP)让每个 rank 独立计算两项: 1. 本段 token 对输入 state 的累计 transition; 2. 从零状态出发生成的本地 state。 这些 rank-level updates 可结合,因而通过一次固定大小的 all-gather 和有序 prefix scan,精确恢复各 rank 的输入 state。KCP 同步的是固定大小 transition/state fragments;softmax CP 传输的 KV blocks 则随序列增长。 ### 9.4 MXFP4 QAT 的准确边界 MXFP4 的生效区间是 post-training:从 SFT 开始贯穿全程,预训练仍走高精度。 | 模块 | 部署/后训练精度 | |---|---| | Routed expert weights | MXFP4,group size 32 | | Routed expert input activations | MXFP8 | | Attention projections | 更高精度 | | LatentMoE down/up projections | 更高精度 | | Shared experts | 更高精度 | | Router、LM Head、Vision | 更高精度 | RL rollout 与训练共用同一量化方案,让 train 与 inference 的数值路径保持一致。发布配置的 `ignore` 规则同样把 attention、shared experts、非 routed-expert MLP、LM Head、vision tower 和 projector 留在高精度。 ## 10. Agentic RL K3 先用 SFT 建立 agent cold start,再按三个领域与三个 reasoning effort 训练九个专家策略: ```text general tasks × {low, high, max} general agents × {low, high, max} coding agents × {low, high, max} ``` 随后用 Multi-Teacher On-Policy Distillation 合并回一个模型。长任务使用 partial rollout:一部分轨迹完成后先进入更新,其余轨迹暂停并在后续 iteration 恢复,更新节奏因此与长尾任务解耦。 1M agentic trajectory 的系统重点落在跨 iteration 的状态保存: - active decode blocks 留在 GPU; - GPU 驱逐的可复用 prefix 以 write-back 方式进入 CPU DRAM; - KDA states 与对应 MLA KV blocks 一起 offload/prefetch; - scheduler 根据 active/queued requests 和 KV utilization 动态限流; - AgentENV 用 microVM 保存工具环境,支持 pause/resume、fork 和增量 snapshot。 ## 11. 推理系统:Prefill 与 Decode 分别做什么 ### 11.1 Prefill Prefill 处理本轮未命中的 prompt/视觉 tokens: 1. MoonViT-V2 编码图像/视频并投影到共享 embedding; 2. KDA 层生成 q/k/v、ShortConv state、decay 和 write gate; 3. FlashKDA 在 chunk 内并行,在 chunk 间传播 recurrent state; 4. MLA 层对完整可见 prefix 执行 global attention 并写入 MLA cache; 5. 每层执行 AttnRes 与 Dense/Stable LatentMoE; 6. 保存最终 KDA states、ShortConv states 和 MLA KV 供 decode 使用。 KDA kernel 的 chunk 是算子内部的并行单位,与 serving scheduler 的 chunked prefill 分属两个层面。超长 prefill 时纯 TP 只切 heads,递推链长度保持原样;K3 因此叠加 device 内与跨设备 context parallelism 来切 sequence。 ### 11.2 Decode Decode 每步只推进新 token: 1. KDA 读取并原地更新固定 recurrent state; 2. MLA 把新 latent KV 追加进 cache,并读取历史做 global attention; 3. AttnRes 从缓存的 block representations 与当前 block partial sum 选择输入; 4. Stable LatentMoE 为 token 选择 16 个 routed experts,加上 2 个 shared experts; 5. LM Head 产生 logits,采样后进入下一步。 所以 K3 的 cache 是混合形态:69 个 KDA 层持有固定 state,24 个 MLA 层持有随上下文增长的 KV。 ### 11.3 MTP / EAGLE-3 与 KDA 回滚 报告称 K3 预训练了一个与 backbone block 同结构的 MTP layer,post-training 时将它微调为 EAGLE-3-style draft model,训练时展开 7 steps。 投机验证给 KDA 带来一条额外约束:state 随每个 draft token 原地推进,reject 后需要重建,而 KV Cache 只需截断。生产方案只缓存体积更小的 projected inputs;验证结束后在片上 replay 被接受的 token,重建正确 state,再写回 verified/bonus token 状态。 发布的目标模型 `config.json` 中 `num_nextn_predict_layers=0`,即公开 target checkpoint 把 next-N layer 留在配置之外;报告中的 draft serving 路径应视为独立的 draft artifact 与系统能力,实际开启状态需另行确认。 ## 12. 混合 Cache 与 Prefix Reuse K3 prefix 只有在同一 token 边界同时具备 MLA KV 和全部 KDA state checkpoint 时才可复用。 ### 12.1 统一 paged pool 生产系统把两种数据放进同一 paged block pool: - MLA pages:逐 token 增长; - KDA pages:每请求固定 state,heads 连续存放; - allocation、reference count、eviction 使用同一实现; - P/D 两端 TP 规模有差异时,在 transfer path 做 re-layout。 “统一 pool”统一的是生命周期与分配器,两种 payload 各自保留自己的形态。 ### 12.2 hash 粒度与物理页解耦 KDA checkpoint 体积很大,保存粒度因此比 token block 粗。报告把物理页扩大到 1024–6144 tokens,把 prefix hash endpoint 保持在更细粒度,例如 512 tokens: ```text 6144-token physical page └─ 12 × 512-token hash endpoints └─ 只在部分 endpoint 保存 KDA checkpoint ``` 查找先匹配 MLA hash,再要求每个 KDA cache group 在同一边界都有 checkpoint;最终命中二者共同满足的最长边界。conversation-turn boundaries 是保留 KDA checkpoint 的自然位置。 并发下还必须保证:命中块先统一 pin;正在分配或复制中的块推迟到完成后才参与匹配;任一 KDA group 驱逐 checkpoint 时,其 sibling checkpoints 一起失效。 ## 13. FlashKDA 与专用 Kernel ### 13.1 Training / Prefill kernel 公开的 FlashKDA 是 CUTLASS chunkwise kernel,面向 SM90+、CUDA 12.9+。K3 版本使用 16-token chunk: - 16-token 累计 decay 可安全落在 BF16 范围; - $16\times16$ inverse 可以用较便宜的 Neumann-series; - token-parallel K1 与 head-parallel recurrent K2 拆成两个 kernel,各自匹配自己的并行维度; - state 在片上以 BF16 保存,state update 使用 FP32 FMA; - K2 用寄存器内 transpose,减少 shared-memory round trip。 FlashKDA 仓库暴露的 state shape 是 `[B, H, V, K]`,当前要求 `K=V=128`,与 K3 发布配置一致。 ### 13.2 Decode kernel 生产 decode kernel 把 ShortConv、输入归一化、gate、KDA recurrence 和输出归一化放进一个 recurrent loop;投机解码还把 replay、bonus token 与下一 draft window 合并。优化目标是压低大 state 的 HBM 往返次数,token 维并行度维持原样。 ### 13.3 AttnRes 与 MoE kernel - AttnRes Prefill:TP all-reduce 拆成 reduce-scatter + all-gather,在中间对 sequence-sharded hidden states 执行 intra-block kernel,让每个 TP rank 只持有自己分片的 block representations; - AttnRes Decode:inter-block pass 放 side stream,与主流独立工作重叠;intra-block merge、partial sum 和 RMSNorm 融进 TP all-reduce; - LatentMoE:down-projection 与 router 合成一个 GEMM;latent weights 跨 rank 切分,all-gather 融入 GEMM epilogue;小 batch expert decode 使用 token-centric weight streaming。 ## 14. 官方参考实现与生产实现的边界 发布在 Hugging Face 的 `modeling_kimi_linear.py` 很适合核对控制流: - `q_len > 1` 调 `chunk_kda`,cached single-token decode 调 `fused_recurrent_kda`; - `KimiDynamicCache` 同时维护 KDA recurrent/conv states 与 attention cache; - layer 依据配置在 `KimiDeltaAttention` 和 `KimiMLAAttention` 间切换; - AttnRes 确实在 attention 前、MoE 前和最终输出处执行。 这份 Transformers 代码的定位是可读参考路径,与技术报告中的生产 serving engine 分属两条实现:参考 MLA path 先展开 head-specific K/V 再交给通用 cache,生产路径缓存 latent 表示并统一管理 KDA/MLA pages。显存与性能数字应以生产路径为准。 ## 15. 目前能下的结论 1. K3 的核心是 `KDA state + 周期性 global MLA + depth attention + latent experts` 的组合,单个 KDA 算子只是其中一环。 2. KDA 把大多数层的历史从逐 token cache 变为固定状态,而每请求状态本身很大,系统优化重点也从“扫描长 KV”转向“少搬大 state”。 3. Gated MLA 承担有限状态之外的全局内容检索,属于架构必需项。 4. Stable LatentMoE 用 0.5× latent width 支撑 top-16/896 极稀疏路由;RMSNorm、SiTU-GLU、QB 分别控制尺度、激活和负载。 5. K3 的 1M context 能力由 NoPE/KDA、渐进长上下文训练、KCP、混合 prefix cache 和 fleet scheduling 共同支撑,attention 公式只是其中一层。 6. 公开资料足以建立结构和成本模型;生产 TP/EP 配置、端到端 TTFT/TPOT 以及各模块对 2.5× scaling efficiency 的独立贡献,需要进一步实测。 ## 后续验证问题 - 发布权重是否另有官方 EAGLE-3 draft artifact,其接受率和实际 speculative window 是多少? - 生产 MLA page 的真实 bytes/token、KDA checkpoint dtype 与每请求 resident state 是多少? - KDA/MLA cache group 如何映射到 TP、P/D 节点和跨节点 transfer heads? - FlashKDA、production recurrent decode kernel 与开源 vLLM/SGLang 路径之间有哪些功能和性能差距? - 在相同质量、硬件和并行配置下,K3 hybrid 相对 full MLA / DSA 的 FLOPs、HBM 和端到端收益各是多少? 这些问题适合进入下一阶段仿真与 Trace 验证;本文先冻结架构和执行语义。 ## 相关页面 - [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/) — KV 共享、压缩、稀疏与递推状态的统一坐标。 - [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/) — latent KV、矩阵吸收与 serving layout。 - [GDN 与 Chunked Prefill](https://weigao.cc/ai-systems/llm-inference/gdn-chunked-prefill/) — 递推 attention 和两类 chunk 的区别。 - [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/) — EP dispatch-compute-combine 数据流。 - [推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) — TP/PP/EP/CP 的作用域。 - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) — MXFP4/MXFP8 的格式与运行时边界。 - [Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) — fusion、HBM 流量和并行度判断。 - [投机解码](https://weigao.cc/ai-systems/llm-inference/05-speculative-decoding/) — draft/verify/accept 的通用原理。 ## 主要来源 - [Kimi K3 Technical Report](https://arxiv.org/abs/2607.24653) - [Kimi K3 官方仓库](https://github.com/MoonshotAI/Kimi-K3) - [Kimi K3 发布配置](https://huggingface.co/moonshotai/Kimi-K3/blob/main/config.json) - [Kimi K3 Transformers 参考实现](https://huggingface.co/moonshotai/Kimi-K3/blob/main/modeling_kimi_linear.py) - [Kimi Linear](https://arxiv.org/abs/2510.26692) - [Attention Residuals](https://arxiv.org/abs/2603.15031) - [FlashKDA](https://github.com/MoonshotAI/FlashKDA) - [MoonEP](https://github.com/MoonshotAI/MoonEP) - [DeepSeek-V3.2 / DSA](https://arxiv.org/abs/2512.02556) --- ## cmux 一键复刻配置 > Source: https://weigao.cc/toolbox/tools/cmux-setup/ > Date: 2026-08-01 > Tags: cmux, terminal, dotfiles, macos, ssh 在一台新 macOS 机器上复刻 cmux 环境:纯白内容卡 + 暖杏侧栏、agent 休眠、原生 SSH 工作区(标题跟随远端目录)。 > **2026-08-05 变更:这份配置不再用侧栏分组。** 折叠加嵌套之后侧栏反而更难扫,改成一行一个会话的平铺列表。§1 的复刻块已经去掉 `workspaceGroups` 和建组步骤,两个建组快捷键(`⌃⌘G` / `⌘⇧G`)都解绑了。分组的机制说明和实测坑留在 [§2](#侧栏分组已不用机制留档) 和 [§3.4](#34-分组这套东西四个坑连在一起) 备查 —— 那些结论对 cmux 本身仍然成立,只是这套配置不用了。 **用法**:把 §1 的整块复制粘给 Claude Code。它自包含——所有配置全文和约束都在块内,不依赖本地文件。§2 之后是给人看的参考,复刻时不必读,但 **§3 的坑清单值得先扫一遍**,那是这套配置里唯一无法从文档推导出来的部分。 > 适用环境:macOS (Apple Silicon),cmux 装在 `/Applications/cmux.app`,14" 屏(部分选择是为窄屏做的,见 §2)。 > 配置快照:2026-08-01,cmux 0.64.20 (build 100)。 > 同一台机器的 shell 侧配置见 [Zsh 一键复刻配置](https://weigao.cc/toolbox/tools/zsh-setup/),两份合起来才是完整环境。 --- ## 1. 一键复刻 Prompt(唯一入口,整块复制) ```` 你是我的环境配置助手。请在这台 macOS (Apple Silicon) 机器上复刻我的 cmux 配置。 所需信息全在本提示词内,不要去读任何本地文件。 【总约束】 1. 动手前备份:~/.config/cmux/cmux.json 和 ~/.config/ghostty/config 若已存在, 各复制一份为 *.bak.<今天日期时分秒>,绝不覆盖未备份的文件。 2. 配置写 ~/.config/cmux/cmux.json(primary)。~/.config/cmux/settings.json 是 legacy, 不要往里写,两个文件都有同名键会打架。用 `cmux config paths` 可确认地位。 3. 改完执行 `cmux reload-config` 生效(同时重载 cmux + ghostty 配置,不需要重启 app)。 校验用 `cmux config doctor`。 4. cmux 自带 ghostty CLI,路径 /Applications/cmux.app/Contents/Resources/bin/ghostty, 可用 `+list-themes` 预览主题。cmux 命令在 /Applications/cmux.app/Contents/Resources/bin/cmux。 5. cmux.json 是 JSONC(允许注释和尾逗号)。但 `cmux config doctor` 对尾逗号宽容, 容易漏;写完请额外用严格 JSON 解析器验一遍(把 // 注释行剥掉再 json.loads)。 【第一步:~/.config/ghostty/config】 终端配色/字体/内边距/起始目录由 ghostty 配置控制,cmux 直接读它。写入以下内容: # —— 日夜自动切换 —— theme = light:GitHub Light Default,dark:GitHub Dark Dimmed # —— 新工作区/窗口的起始目录 —— # 生效前提:cmux.json 里 app.workspaceInheritWorkingDirectory = false # 可填绝对路径 / ~/xxx / home / inherit working-directory = ~/Documents/_work/99_code # —— 内边距留白,配合悬浮卡片观感 —— window-padding-x = 12 window-padding-y = 10 window-padding-balance = true # background-opacity 保持默认 1(不透明)。别试透明毛玻璃,原因见下方说明。 【第二步:~/.config/cmux/cmux.json】 写入以下内容(注释保留,它们记录了为什么这么选): { "$schema": "https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json", "schemaVersion": 1, "app": { "appearance": "system", "minimalMode": true, "workspaceInheritWorkingDirectory": true, "forkConversationDefaultDestination": "newTab", "globalFontMagnification": 90, "hideTabCloseButton": true, "reorderOnNotification": true, "warnBeforeQuit": true }, "activePaneBorderColor": "#D97706", "sidebarAppearance": { "matchTerminalBackground": false, "lightModeTintColor": "#E8955A", "darkModeTintColor": "#E8B48A", "tintOpacity": 0.11 }, "workspaceColors": { "indicatorStyle": "lift", "selectionColor": "#FDF8F3" }, "canvas": { "paneGap": 20 }, "sidebar": { "showProgress": true, "showLog": true, "showPorts": true, "showPullRequests": true, "showSSH": true, "showBranchDirectory": false, "showNotificationMessage": true, "notificationMessageLineLimit": 3, "beta": { "workspaceTodos": { "checklistStyle": "inline" } } }, "markdown": { "fontSize": 16, "maxWidth": 820, "fontFamily": "" }, "fileEditor": { "wordWrap": true }, "fileExplorer": { "doubleClickAction": "preview" }, // 不用侧栏分组,所以没有 workspaceGroups 块(它只管分组头的图标/颜色/落位)。 // 两个建组快捷键都解绑,免得侧栏又冒出组来。cmux 没有"禁用分组"的开关, // 解绑是能做到的最接近的事;右键菜单和 CLI 不受影响。 "shortcuts": { "bindings": { "newWorkspaceGroup": null, "groupSelectedWorkspaces": null } }, "terminal": { "agentHibernation": { "enabled": true, "idleSeconds": 30, "maxLiveTerminals": 6 }, "uploadCommands": [ { "command": "scp -q -o ControlMaster=auto -o ControlPath=/tmp/cmux-ssh-%r@%h:%p -o ControlPersist=300 ${CMUX_UPLOAD_PORT:+-P $CMUX_UPLOAD_PORT} ${CMUX_UPLOAD_IDENTITY_FILE:+-i $CMUX_UPLOAD_IDENTITY_FILE} $CMUX_UPLOAD_SSH_OPTIONS \"$CMUX_UPLOAD_LOCAL_PATH\" \"$CMUX_UPLOAD_DESTINATION:$CMUX_UPLOAD_REMOTE_PATH\" >&2 && printf %s \"$CMUX_UPLOAD_REMOTE_PATH\"", "enabled": true } ] }, "automation": { "workspaceAutoNaming": true, "autoNamingAgent": "auto" }, "notifications": { "dockBadge": true, "showInMenuBar": true, "unreadPaneRing": true, "paneFlash": true, "agentPermissionPrompt": true, "agentTurnComplete": "whenIdle", "agentIdleReminder": true, "suppressOnlyFocusedSurface": true, "sound": "Tink" }, "diffViewer": { "defaultLayout": "unified" }, "commands": [ { "name": "新工作区 · 99_code", "description": "从代码根目录开一个空工作区(显式 --cwd 才可靠,见 §5)", "keywords": ["new", "workspace", "99code", "root"], "command": "cmux workspace create --cwd ~/Documents/_work/99_code" }, { "name": "SSH · dev-env", "description": "标题跟随远端目录", "keywords": ["ssh", "devenv"], "command": "cmux ssh @ --identity ~/.ssh/id_ed25519" }, { "name": "SSH · agent-env", "description": "标题跟随远端目录", "keywords": ["ssh", "agentenv"], "command": "cmux ssh @ --identity ~/.ssh/id_ed25519" }, { "name": "SSH · 重连断掉的远程会话", "description": "只重连不健康的远程工作区(合盖醒来用;见 §3.12)", "keywords": ["ssh", "reconnect", "wake"], "command": "zsh -ic sshreconnect" }, { "name": "SSH · 强制重连全部远程会话", "description": "无条件重连所有远程工作区", "keywords": ["ssh", "reconnect", "force"], "command": "zsh -ic 'sshreconnect -f'" } ] } 把 commands 里的 // 和 --cwd 路径换成本机实际值。 重连预设调用的 sshreconnect 函数在第三步的 ~/.zshrc 里,逻辑只维护这一份。 【第三步:~/.zshrc 追加】 # ---- cmux diff 快捷方式 ---- alias cdiff='cmux diff --unstaged' # 未暂存改动 alias cdlast='cmux diff --last-turn' # agent 这一轮改了什么 alias cdbr='cmux diff --branch' # 当前分支 vs merge base # ---- cmux 原生 SSH 工作区 ---- # 故意不传 --name:显式命名会永久盖住远端发来的 OSC 标题(见 §3.6)。 # 侧栏里认哪台机器,看工作区下面的 SSH 明细行(sidebar.showSSH)。 devenv() { cmux ssh @ --identity "$HOME/.ssh/id_ed25519" "$@"; } agentenv() { cmux ssh @ --identity "$HOME/.ssh/id_ed25519" "$@"; } # ---- 一键重连远程工作区(合盖过夜后救活,见 §3.12)---- # 默认只重连不健康的(跳过正常连接,不打断正在跑的 agent);-f 强制全部。 # 实测重连不丢会话(远端 session id 前后一致)。判活用 state+daemon+proxy, # 不看 remote.heartbeat(那字段会冻住不刷新,见 §3.12)。 sshreconnect() { local cmux; cmux=$(command -v cmux) || { echo "找不到 cmux CLI"; return 1; } local force=""; [ "$1" = "-f" ] && force="1" "$cmux" list-windows --json 2>/dev/null | FORCE="$force" python3 -c ' import sys, json, os, subprocess force = os.environ.get("FORCE") == "1" cmux = "cmux" try: wins = json.load(sys.stdin).get("windows", []) except Exception: wins = [{"ref": None}] refs = [w.get("ref") for w in wins] or [None] targets, skipped = [], 0 for wref in refs: cmd = [cmux, "workspace", "list", "--json"] + (["--window", wref] if wref else []) try: d = json.loads(subprocess.run(cmd, capture_output=True, text=True).stdout) except Exception: continue for w in d.get("workspaces", []): r = w.get("remote", {}) if not r.get("enabled"): continue healthy = (r.get("state")=="connected" and r.get("daemon",{}).get("state")=="ready" and r.get("proxy",{}).get("state")=="ready") if healthy and not force: skipped += 1; continue targets.append((wref, w["ref"], r.get("destination") or "?", r.get("state"))) if not targets: print(f" 没有需要重连的(跳过 {skipped} 个健康连接)" if skipped else " 没有远程工作区"); sys.exit(0) for wref, ws, dest, state in targets: cmd = [cmux, "workspace", "reconnect", "--workspace", ws] + (["--window", wref] if wref else []) ok = subprocess.run(cmd, capture_output=True, text=True) print(f" {\"OK\" if ok.returncode==0 else \"失败\"} {ws} {dest} (原 state={state})") if skipped: print(f" (另跳过 {skipped} 个健康连接,-f 可强制全部)") ' } 【第四步:远端 daemon(国内网络必做,否则 cmux ssh 一定超时)】 cmux ssh 要在远端跑 cmuxd-remote daemon,这个二进制不在 app 包里,要从 GitHub Releases 下。 国内直连 release-assets.githubusercontent.com 会超时,报 "Remote daemon bootstrap failed: 请求超时"。做法: a) 查目标平台的资源名、下载 URL、期望 sha256、缓存路径: cmux remote-daemon-status --os linux --arch amd64 b) 用能翻的网络(浏览器/代理)下载那个 cmuxd-remote-linux-amd64。 c) 放到 ~/.local/state/cmux/remote-daemons/<版本号>/linux-amd64/cmuxd-remote 并 chmod +x。 d) 再跑一次 a),应显示 cache exists: yes / cache verified: yes。 【重要】下载后必须核对 sha256,不能只看 HTTP 200。实测同一条内网链路上 366B 的 checksums.txt 完好,而 5.9MB 的二进制被截断成 2.7MB 却仍返回 200。 macOS `file` 报 "too large section header offset" 就是截断征兆。 缓存路径带版本号,cmux 升级后失效,同样的超时会再犯一次。 【第五步:远端 shell 的标题联动(可选但推荐)】 让 cmux 侧栏标题跟着远端当前目录实时变。先确认远端交互 shell 是 bash 还是 zsh (注意:登录 shell 可能是 bash,但 ~/.bashrc 里有 exec zsh 把它换掉, 这种情况要改 ~/.zshrc,往 .bashrc 末尾加东西永远执行不到)。 zsh 版本,追加到远端 ~/.zshrc 末尾(必须在 oh-my-zsh / powerlevel10k 之后, 否则会被它们的标题设置盖掉): # >>> BEGIN cmux-title (整段删除即可还原) : ${CMUX_HOST_LABEL:=dev-env} __cmux_set_title() { printf '\033]0;%s:%s\007' "$CMUX_HOST_LABEL" "${PWD/#$HOME/~}" } autoload -Uz add-zsh-hook add-zsh-hook precmd __cmux_set_title # <<< END cmux-title bash 版本用 PROMPT_COMMAND 前插,并加 [ -n "$PS1" ] 守卫避免污染非交互会话。 改远端配置前先备份,且用标记注释包裹以便整段删除。 【第六步:验证】 cmux config doctor # 两个配置文件都应 OK,keys 里不该有 workspaceGroups cmux reload-config # 应返回 OK Reloaded config cmux themes list # 确认 light/dark 主题名生效 cmux workspace-group list # 应返回 No groups —— 这套配置不用分组 source ~/.zshrc && devenv # 开远程工作区,侧栏应显示主机 + "已连接" 日常一条要记住:笔记本过夜合盖后,SSH 工作区可能卡在"connected 但传输已断", 不会自愈(详见 §3.12)。救活是一条命令,远端会话零损失: cmux workspace reconnect --workspace 【完成后告诉我】 - 侧栏是暖杏底 + 纯白内容卡 + 选中项浮起吗 - devenv 连上后,在远端 cd 时侧栏标题跟着变吗 ```` --- ## 2. 配置分区说明 配置分两个文件,边界很清楚: | 归属 | 文件 | 管什么 | |---|---|---| | 终端渲染 | `~/.config/ghostty/config` | 主题、字体、内边距、透明度、起始目录 | | cmux 自身 | `~/.config/cmux/cmux.json` | 侧栏、图标、通知、agent 行为、SSH 预设 | ### 配色:纯白内容卡 + 暖杏侧栏 内容区用 `GitHub Light Default`(纯白),侧栏叠一层暖杏 tint(`#E8955A` @ 11%,实际渲染约 `#F1E8E6`)。选中项用 `indicatorStyle: lift` + `selectionColor: #FDF8F3`(暖象牙白)——比侧栏亮所以"浮起"效果成立,比纯白柔和所以跟暖杏同色系更搭。 强调色 `#D97706`(琥珀)只用在活动面板边框,且只在分屏时可见。 ### 为窄屏做的六个选择 14" 屏上这几项和大屏的最优解相反: - `app.minimalMode: true` —— 隐藏工作区标题栏,省一行垂直空间 - `app.globalFontMagnification: 90` —— UI 整体缩到 90%(终端、标签、侧栏、浮层;不影响浏览器里渲染的网页) - `app.forkConversationDefaultDestination: newTab` —— Fork Conversation 默认去新 tab 而不是右侧分屏。右键 tab 的子菜单仍可选任意方向 - `diffViewer.defaultLayout: unified` —— 左右分栏在窄屏每侧太窄,代码全折行反而难读 - `sidebar.notificationMessageLineLimit: 3` —— 默认 12 行,一个工作区能吃掉侧栏 1/5 高度 - `app.hideTabCloseButton: true` —— 单 surface 时那条横向标签栏没法隐藏,只能去掉 ✕ 减少噪音 窄屏上与其分屏,不如用 `⌘B` 隐藏侧栏、`⌘T` 开新 tab(每个都是全宽)、`⌘⇧回车` 临时全屏当前面板。 ### 用法:一行一个会话,只走纵向 cmux 的四层模型是 Window / **Workspace** / Pane / **Surface**。关键点:**侧栏的一行就是一个 workspace,没有比它更轻的"会话"对象**——cmux 内部就把侧栏这些行叫 `tab`(`cmux sidebar-state` 返回的字段名是 `tab=`),所谓 "vertical tabs" 指的正是它们。 所以"一个工作区放一个会话、在侧栏纵向往下加"是正统用法,不是绕路: - `⌘N`(`newTab`)→ 新建侧栏一行 - `⌘T`(`newSurface`)→ 在当前工作区内加一个**横向** tab —— 不想要横向就别用它 - `⌘W` 关 tab。`app.keepWorkspaceOpenWhenClosingLastSurface` 保持默认 `false`,关掉最后一个 surface 会连整行一起收掉,不留空壳 单 surface 时那条横向 surface 标签栏**没有配置项能隐藏**(搜过整个 schema)。只有两个相关开关:`app.hideTabCloseButton`(去掉关闭按钮,减少噪音,已开)和 `ui.surfaceTabBar.buttons`(能定制按钮,但是 nightly 特性)。`minimalMode` 隐藏的是工作区标题栏,不是这条。 ### 侧栏分组(已不用,机制留档) > **这套配置 2026-08-05 起不用分组了。** 用过四天的结论:分组确实能把侧栏收短,但代价是多一层折叠——想找某个会话,先要记住它在哪个组、那个组是不是折着的。平铺列表虽然长,扫一眼就到底,反而更快。 > > 下面的机制说明和 CLI 速查都还准(对 cmux 0.64.20 成立),留着备查。**注意 cmux 没有"禁用分组"的开关**:`workspaceGroups` 只管分组头的图标/颜色/落位,分组本身是侧栏内置行为。所以"关掉"只能是三件事:解散已有的组(`ungroup`,保留成员)、删掉 `workspaceGroups` 配置、解绑两个建组快捷键。 核心模型一句话:**每个分组由一个 anchor 工作区拥有,anchor 那一行就是分组头,没有额外的头行。** 点标题聚焦 anchor 的面板,点箭头折叠。 三个快捷键(前两个在这份配置里都已用 `shortcuts.bindings` 解绑): | 快捷键 | 动作 | 触发条件 | |---|---|---| | `⌃⌘G` | 新建**空**分组 | 无条件,误触就多一个叫「分组 N」的空壳 | | `⌘⇧G` | 把选中的工作区成组 | **必须选中 ≥2 个**,否则不响应 | | `⌃⌘.` | 折叠/展开聚焦的分组 | 没有组时是空操作,所以没解绑 | `⌘⇧G` 和 React Grab 撞键,cmux 只在有多选时才抢这个键,所以平时不受影响;解绑之后彻底让给 React Grab 了。真正容易误触的是 `⌃⌘G` —— 无条件触发,误触过两次。 **新工作区进不进组,UI 和 CLI 不一样**(这条实测出来的,文档没写): | 路径 | 结果 | |---|---| | UI 里活动工作区是 anchor 或成员时按 `⌘N` | 新工作区**进组** | | 分组头 hover 出来的 `+` 按钮 | 进组,cwd = anchor 的 cwd | | CLI `cmux workspace create` 不带 `--group` | **永不进组** | CLI 那条试了两种可能的暗示方式——`CMUX_WORKSPACE_ID` 指到组内成员、以及当前选中项就在组内——都不进组。要进组必须显式 `--group `(还有 `--group-placement` / `--group-reference`)。 **`cmux ssh` 更彻底:它根本没有 `--group` 参数**(只有 `workspace create` 有)。所以 `devenv`/`agentenv` 这类基于 `cmux ssh` 的函数开出来的远程工作区**天生游离**,不会进任何组,也没有"以后连 .130 的都归 dev-env"这种规则——分组只是一次性把当时的工作区收进壳。想让它自动进组,只能在函数里绕:`cmux ssh` 的 stdout 是 `OK workspace=workspace:N target=... state=...`,解析出这个 ref,再按**组名**(不是运行时会变的 `workspace_group:N`)动态查到 group ref,`workspace-group add` 进去;`add` 幂等所以重连也安全。§1 第三步的 `devenv`/`agentenv` 曾经这么干过,撤掉分组时一起删了 —— 现在就是直接 `cmux ssh`。 组内新建工作区落在哪由 `workspaceGroups.newWorkspacePlacement` 决定:`afterCurrent`(默认,插在活动成员后面)/ `top`(紧跟 anchor)/ `end`(追加到最后)。「一行一个会话往下加」的用法配 `end` 顺序最可预期。还能在某条 byCwd 规则里单独覆盖。 分组的 pin 独立于工作区的 pin,置顶的分组排在所有未置顶的顶层行之上。组名、anchor、pin、折叠状态、颜色、图标都跨重启保留,成员关系存在每个工作区上。 CLI 速查(`` 收 UUID 或 `list` 打印的 `workspace_group:N`): ```bash cmux workspace-group list --json # icon/color 为 null 才是走 byCwd cmux workspace-group create --name x --from a,b # 永远显式 --from(见 §3.4) cmux workspace-group create --name x --from "" # 真空组,不碰现有工作区 cmux workspace-group add --group --workspace # 从别的组移过来也是这条 cmux workspace-group remove --workspace cmux workspace-group set-anchor --group --workspace cmux workspace-group ungroup # 解散,保留成员 cmux workspace-group delete # 连带关闭所有成员,destructive cmux workspace-group set-icon --symbol server.rack # 传 "" 清除,退回 byCwd cmux workspace-group set-color --hex "#1A5276" cmux workspace-group collapse|expand|pin|unpin|focus cmux workspace-group move --to-index 0 # 也支持 --before / --after cmux workspace-group new-workspace [--placement end] ``` 误关了工作区可以用 `reopenClosedWorkspace` 动作救回来。 `byCwd` 每条规则其实有**四个**字段,常用的只有前两个:`icon`、`color`、`newWorkspacePlacement`(单目录覆盖落位)、`contextMenu`(分组 `+` 按钮的右键菜单,schema 与 `ui.newWorkspace.contextMenu` 相同——后者标注为 nightly,这份配置没用)。 ### 侧栏直接显示 agent todo `sidebar.beta.workspaceTodos.checklistStyle` 有两个值,功能本身一直是开的、没有开关,只能选展示方式: - **`inline`**(现用)—— 清单就地在侧栏那行下面展开,扫一眼就知道 agent 在干到第几步 - `popover` —— 从摘要行弹锚定浮层,不占侧栏纵向空间 窄屏上 `inline` 会往下顶其它工作区,如果侧栏工作区多到挤不下,换回 `popover`。 ### markdown 查看器与文件树 内置 markdown 查看器**带 live reload**——`⌘` 点 `.md` 打开后,文件被改(包括 agent 改)会自动刷新,写 wiki 类内容很顺。 - `markdown.fontSize: 16` —— 默认 15,但开了 90% 缩放后要提一档正文才舒服。查看器内 `⌘+` / `⌘-` / `⌘0` 可临时缩放 - `markdown.maxWidth: 820` —— 默认 980 在 14" 上一行太长;中文一行 45 字左右最好读 - `fileEditor.wordWrap: true` —— 默认 false。中文长句没有空格,不换行就得横向滚动 - `fileExplorer.doubleClickAction: preview` —— 保持默认。cmux 内置预览支持 markdown live reload、PDF、图片,比丢给外部应用顺。想双击进外部编辑器要**两处一起设**:改成 `preferredEditor` 并设 `app.preferredEditor` 命令,只改前者会回落到 macOS 默认应用 ### agent 相关 - `agentHibernation`(默认关):活跃 agent 终端超过 `maxLiveTerminals: 6` 时,后台空闲的自动休眠释放内存,回访时用保存的 session 恢复 - `workspaceAutoNaming`:`auto` 模式下每个会话用它自己的 agent 起名(Claude 的用 Claude,Kiro 的用 Kiro) - `notifications.agentTurnComplete: whenIdle`:后台任务真跑完才提示一次,不会中途误报 - `notifications.suppressOnlyFocusedSurface: true`:并行多 agent 时,非聚焦面板的横幅会留着等你看 ### SSH 上传走连接复用 `terminal.uploadCommands` 里那条规则替代内置 scp,加 `ControlMaster=auto` + `ControlPersist=300`。收益是拖 N 个文件只握手 1 次。不加 `-C` 压缩——传的是 `.json.gz`,已经压过。 命令里 scp 输出全转 stderr,stdout 只回远端路径,cmux 会把它插到光标处。想临时停用改 `enabled: false`。 --- ## 3. 踩过的坑 这节是这份文档的主要价值。以下每条都是实测结论,不是文档推导。 ### 3.1 配置文件:settings.json 是 legacy `~/.config/cmux/settings.json` 是旧位置,primary 是 `cmux.json`。两个文件都放同名键会打架。用 `cmux config paths` 确认,`cmux config doctor` 校验。 顺带:`cmux reload-config` 一条命令重载 cmux + ghostty 两份配置,**不需要重启 app**。 ### 3.2 「毛玻璃侧栏 + 不透明白卡」做不到 想复刻 otty 的 Floating Card 那种双层效果(外框半透明透出壁纸、中间白卡实心),在 cmux 0.64.20 上是死路,三个原因叠加: 1. `sidebarAppearance` 只有实色 tint,cmux 没有独立的窗口 vibrancy/material 配置项 2. ghostty 的 `background-opacity` 是**全局**的,一降透明度中间正文会跟侧栏一起被壁纸染色 3. 想用 `background-image` 单独把正文盖白也不行——`background-image-opacity` 是**相对** `background-opacity` 的、封顶 otty 能做是因为它的主题格式把 `[sidebar]` / `[container]` / `[window] material` 分开定义。所以只能在"白卡"和"通透"之间选一个,这份配置选了白卡(`background-opacity = 1`)。 ### 3.3 `selectionColor: null` 不是"自适应中性",是系统蓝 想让选中卡走中性色而把这个字段设 `null`,结果会回退到 cmux 默认的系统蓝。要白色悬浮卡必须显式写颜色。该字段**不分明暗**,只有一个值——所以纯白卡在深色模式下会偏亮扎眼。 ### 3.4 分组这套东西,四个坑连在一起 `workspaceGroups.byCwd` 配的是**侧栏分组头**的图标和颜色。围绕它有四个独立的坑,实测逐个确认过。 > 这份配置现在不用分组了(见 §2),下面四条对 cmux 本身仍然成立,留档。第 (4) 条尤其值得记住 —— 它能在你只想建个组的时候悄悄关掉一个正在跑的 agent 会话。 **(1) 没有分组时规则完全不显示。** 分组**必须显式创建**,不会按 cwd 自动形成。看起来像配置没生效,其实是缺前提。分组是运行时状态,不进 cmux.json,换机器要重建。 **(2) 匹配的是 anchor 的 cwd,不是成员的。** anchor 的 cwd 来源有三条:成组时继承 `--from` 里第一个成员 / CLI 不传 `--cwd` 时继承活动工作区 / `create --cwd ` 显式指定(实测有效,而且**允许不存在的路径**,会原样记下来)。 所以 `cmux ssh` 开的远程工作区是个死角——**它们在本机视角 cwd 是空字符串**,byCwd 永远匹配不上: ``` workspace:1 (本地 anchor) cwd='/Users/.../awp-aggregator' → 规则命中 workspace:11 (SSH 工作区) cwd='' → 任何规则都不命中 ``` 两条出路:`set-icon`/`set-color` 显式设(命令式,换机器丢),或者给远程组建一个 cwd 落在空标记目录里的本地 anchor(声明式,写在 cmux.json 里): ```bash mkdir -p ~/Documents/_work/99_code/_remote/dev-env cmux workspace-group create --name dev-env --from "" \ --cwd ~/Documents/_work/99_code/_remote/dev-env # --from "" = 真空组,不碰现有工作区 cmux workspace-group add --group workspace_group:N --workspace ``` 这份配置原来走的是第二条(`_remote/` 下两个空目录当 anchor),撤掉分组时连 anchor 工作区一起关了。 **(3) 侧栏分组头显示的是 anchor 工作区的标题,不是组名。** 这条最容易误判。`workspace-group create --name X` 会把组名和 anchor 标题一起设成 X,看起来一致;但之后 `workspace-group rename` **只改组名,不动 anchor 标题**,侧栏毫无变化。实测: ``` $ cmux workspace-group rename workspace_group:5 --name zz-renamed OK $ cmux workspace list # anchor 标题没变 workspace:18 zz-test ← 侧栏显示的是这个 ``` 所以 `⌃⌘G` 建出来的 `分组 2` 这类标题,改组名是白费的,得改工作区: ```bash cmux workspace rename --workspace "blogv2" ``` **(4) `create` 不传 `--from` 会抓走你正在看的工作区。** CLI help 写的是 "Defaults --from to the active sidebar selection / caller workspace"——**选中项优先于调用方**。实测在一个 SSH agent 会话被选中时跑: ```bash cmux workspace-group create --name zz-test --cwd ~/somewhere # 没传 --from # → 新组成员是 [新 anchor, workspace:5],workspace:5 是正在跑 agent 的 SSH 会话, # 而且它被从原来的 dev-env 组里拽出来了(一个工作区只能属于一个组) ``` 危险在下一步:`workspace-group delete` 会**连带关闭组内所有工作区**。顺手 delete 就等于关掉那个活会话。永远显式传 `--from`;要真空组就 `--from ""`;解散用 `ungroup`(保留成员),`delete` 才是 destructive 的。 ### 3.5 工作区标题里的 session-id 后缀无法关掉 Claude Code 的工作区标题会带 ` · ` 后缀(如 `· 78f819f5-9bcb-4f`)。把整个 schema 搜遍了,**没有任何开关**能去掉它。来源是 cmux 读 Claude Code 的 session JSONL 文件名。 唯一可靠解是手动改名(`⌘⇧R` 或 `cmux workspace-action --action rename`),文档明确写了手动命名永久优先。 ### 3.6 `--name` 会永久压死 OSC 标题 这条和 3.5 是同一个机制的两面。给 `cmux ssh` 传 `--name` 相当于手动命名,会把远端通过 OSC 转义序列发来的标题彻底盖住。所以**要标题跟随远端目录,就不能传 `--name`**,二者只能选一个。 ### 3.7 远端命令没法自动执行 想让 `cmux ssh` 连上后自动跑一条远端命令(比如 `zellij attach`),两条路都不通: | 写法 | 结果 | |---|---| | `--command "…"` | **压根不执行**(用远端标记文件验证过:工作区能连上,命令不跑) | | `-- <远端命令>` | 命令**会**执行,但绕过 cmux 远端 shell 集成 → 永远卡在 `[ssh:connecting]`、持久 PTY 建立不起来、转义序列漏成乱码(终端里出现 `^[[?997;2n`) | 结论:连上后手敲。而且 cmux 自己的持久 PTY 已经做了 zellij 的保活那份活,新活儿不必再套一层。 ### 3.8 远端 shell 可能不是 passwd 里那个 `getent passwd` 和 `$SHELL` 都显示 `/bin/bash`,但 `~/.bashrc` 第 33 行有 `exec zsh -l`——交互环境实际是 zsh。往 `.bashrc` **末尾**追加的东西在 `exec` 之后,永远执行不到。 判断方法:看报错格式。`文件:行号: command not found: xxx` 是 zsh 的格式,bash 的长得不一样。 ### 3.9 内网下载大文件必须核对 sha256 见 §1 第四步。5.9MB 二进制被截断成 2.7MB 但仍返回 HTTP 200,同链路 366B 小文件完好。**不能信 HTTP 200**。 ### 3.10 SSH 会话大多不用管,但远端中继端口会被陈旧连接卡住 正常情况不用清理:一轮下来开关 6 个 SSH 工作区,之后 `ssh-session-list` 只剩 1 个(对应唯一存活的工作区),零孤儿,生命周期跟着工作区走。 **但有一个例外会真的出问题。** 报错长这样: ``` Remote daemon error: Remote SSH relay... Error: remote port forwarding failed for listen port 57279 (retry in 2s) ``` 成因链条: 1. cmux 用 SSH 远端端口转发(`-R`)建中继,端口号记在 `~/.cmux/relay/.slot` 一类文件里 2. 陈旧的 SSH 连接(实测有存活 10~12 小时的)仍占着那个端口 3. cmux **自带**一段 `cmux_stale_relay_listener_cleanup` 脚本处理这种情况——用 `lsof` 找出占端口的 sshd,核对 relay 元数据确认无主后 kill 掉 4. 但 `-R` 转发的监听套接字是由**root 身份**的 sshd 监控进程创建的。没有 root 权限时,`lsof` 和 `ss -tlnp` 都**看不到属主** 5. 脚本找不到 PID → `[ -n "$cmux_listener_pids" ] || exit 0` → 静默空转 → 无限重试 **影响范围有限**:终端本身是好的,坏掉的只是中继功能(内置浏览器从远端出网)。不影响敲命令。 **为什么每次重启 Mac 都会撞**:远端 sshd 若没配 `ClientAliveInterval`(默认 `0` = 从不探测),你的机器一关机,远端那条连接不会被回收,实测能挂 10~12 小时不动。有 root 的话让运维配上 `ClientAliveInterval` 是根治办法。 **免密 sudo 帮不上 cmux**:那段自愈脚本直接调 `lsof`,源码里没有任何 `sudo`。配了 sudo 它也不会用,只方便你手动查(`sudo ss -tlnp | grep `)。为这个开 root 免密不值。 **不用 root 的回收办法**:陈旧的 `sshd: @notty` 进程归你自己所有,可以直接 kill。判据是**有没有 `cmuxd-remote` 子进程**——有=在用,无=陈旧。放进 `~/.zshrc`: ```bash # sshgc 只列出 # sshgc -f 真的杀 sshgc() { local host="${1:?用法: sshgc [-f]}" force="${2:-}" ssh -o BatchMode=yes -o ConnectTimeout=8 "$host" "FORCE='$force' bash -s" <<'REMOTE' self_chain=""; p=$$ while [ "$p" -gt 1 ] 2>/dev/null; do self_chain="$self_chain $p"; p=$(ps -p "$p" -o ppid= 2>/dev/null | tr -d ' '); [ -z "$p" ] && break done found=0 for pid in $(ps -eo pid,user,args --no-headers | awk -v u="$USER" '$2==u && /sshd:.*@notty/ {print $1}'); do case " $self_chain " in *" $pid "*) continue ;; esac # 别把自己这条连接杀了 kids=$(ps -eo ppid,args --no-headers | awk -v p="$pid" '$1==p && /cmuxd-remote/ {c++} END{print c+0}') [ "$kids" != "0" ] && continue et=$(ps -p "$pid" -o etime= 2>/dev/null | tr -d ' '); found=$((found+1)) if [ "$FORCE" = "-f" ]; then kill "$pid" 2>/dev/null && echo " 已杀 pid=$pid (运行 $et)" else echo " 陈旧 pid=$pid (运行 $et) —— 加 -f 才真的杀"; fi done [ "$found" = "0" ] && echo " 没有陈旧连接" REMOTE } ``` 排除当前连接祖先链那段是必须的,否则脚本会把自己所在的 ssh 连接一起杀掉。 **还有一种撞法是自冲突,`sshgc` 治不了**:同一条**活着的**连接,PTY 桥断开重连后 cmux 会在**同一个端口**重建转发,撞上自己先前那个还没释放的转发。实测表现是杀光所有陈旧连接后端口仍被占,而剩下的唯一持有者就是当前在用的那条。这种只能等——关掉该工作区或重启 cmux 即可,期间终端功能不受影响。 **别做的事**:不要凭 `--slot` 和 session id 的前缀是否匹配去判断哪个 `cmuxd-remote` 是孤儿然后杀掉——见 3.11。 ### 3.11 `--slot` 不等于 session id,别靠前缀匹配判断孤儿 `cmuxd-remote` 进程的 `--slot ssh-` 是 **SSH 连接槽位**,而 `ssh-session-list` 输出的 session id 是 `ssh--` 形式的**另一套标识**。**一个 slot 可以承载 ID 完全不相关的会话。** 踩过的坑:看到某个 `--persistent-server` 进程的 slot(`ssh-412642b9…`)跟当前唯一会话 ID(`ssh-5D4FF9A3…`)前缀不匹配,就判定它是孤儿并 kill 掉——结果它正在服务那个活着的工作区,终端被重启,**1 MB 回滚内容和正在跑的 agent 会话全丢**。 `--persistent-server` 的 `ppid=1` 也不是孤儿的证据:持久化设计本来就会让它脱离父进程,这样断连才不掉线。 要判断某个远端 daemon 能不能动,可靠依据只有 `ssh-session-list --workspace ` 逐个工作区对照,而不是进程参数。 ### 3.12 过夜合盖后不会自动重连,且 `heartbeat.age` 是假指标 这条推翻了两个先前的判断,实测过程记在这。 **现象**:笔记本合盖过夜(约 9 小时),早上打开,三个 SSH 工作区侧栏都挂着红字: ``` Remote proxy to @ unavailable: Remote daemon transport failed: daemon transport keepalive timed out (retry in 3s) ``` 注意这**不是** 3.10 那个中继端口坑——文案是 `daemon transport keepalive timed out`(传输层 keepalive 超时),不是 `listen port NNNNN`(端口冲突)。成因是合盖期间本机到远端的网络中断,cmux 的自动重连"暂停"了,醒来没有自己恢复。 **别信 `remote.heartbeat.age`**:`workspace list --json` 里三个会话全写 `state=connected` / `daemon=ready` / `proxy=ready`,但 `heartbeat.age_seconds` 冻在断连时刻、8~9 小时不动。一度拿这个 age 判定"连接已死"——错的。真实流量能跑通时这个字段照样不刷新,它只统计某种独立心跳、不随数据流更新。**判活只能靠读屏或发探针命令看回显**,不能看这个字段,也不能看 `state=connected`(那是缓存态)。 **远端会话其实都活着**:`ssh-session-list --workspace ` 显示三个远端持久会话 `attachments=1`、`scrollback_bytes=1048576`(1 MB 满额)全在。丢的只是本机↔远端这段传输,远端 PTY 和昨晚跑的 agent 没受影响。 **正确处理是 reconnect,不是 sshgc**: ```bash cmux workspace reconnect --workspace # 逐个救;救活后昨晚的东西原样都在 cmux mark-notification-read --all && cmux dismiss-notification --all-read # 清红字横幅 ``` 工作区多了逐个 reconnect 很烦,包成一个函数(§1 第三步已含):`sshreconnect` 只重连不健康的、跳过正常连接(不打断在跑的 agent),`sshreconnect -f` 强制全部。命令面板也有对应两条预设。实测重连**不丢会话**——远端 session id 前后一致。判活务必用 `state`+`daemon`+`proxy` 三个字段,别用 `heartbeat`。 `sshgc` 在这里没用——它是给"远端陈旧连接占端口"用的(3.10),而这里远端 daemon 好好的,是本机侧传输要重建。`clear-notifications` 也清不掉这两条红字:它们是 `none` 作用域的未读通知,得先 `mark-notification-read --all` 再 `dismiss-notification --all-read`。 **能不能根治**:不能一劳永逸,但能减轻。合盖断网这段传输必然中断(物理决定,任何 SSH 客户端都躲不掉)。三个改善入口:① 记住 `reconnect` 命令(零成本,推荐);② `cmux ssh` 加 `--ssh-option ServerAliveInterval=30 --ssh-option ServerAliveCountMax=3`,只对**短暂抖动**有效、对整夜断网无效(网络不通探测包也发不出),为整夜合盖场景改它不值;③ 远端 sshd 配 `ClientAliveInterval`(要 root,且治的是 3.10 的陈旧连接不是这条)。实测本机 `~/.ssh/config` 无 keepalive、远端 `ClientAliveInterval=0`(从不探测),所以"死连接挂整晚"是必然而非偶发。 --- ## 4. 实测数据 | 项目 | 实测值 | |---|---| | 远端 daemon 进程 | 约 2 个/连接,合计 16.1 MB RSS | | 占远端内存比例 | 0.001%(该机 1487 GB 内存) | | 远端磁盘 `~/.cmux` | 6.2 MB,其中 6.0 MB 是 daemon 二进制本身(别删,删了要重推) | | 断连重连 | `workspace disconnect` → `reconnect` 后 session ID 不变,scrollback 从 93 KB 增长到 99 KB 完整保留 | | 过夜合盖后 | 三个会话侧栏显示 `connected` 但传输实际已断(见 §3.12),`ssh-session-list` 显示远端持久会话 `attachments=1`、scrollback 1 MB 满额全部存活;`workspace reconnect` 后恢复,昨晚跑的 agent 全在 | | 被截断的下载 | 期望 5.9 MB,实收 2.7 MB,HTTP 200 | **远端进程不会因本机断网而死**:远端跑的是远端进程,PTY 和回滚由远端 `cmuxd-remote` 持有。短暂断网(合盖几分钟、切网络)cmux 能自动重连接回同一会话,scrollback 不丢。 **但"过夜合盖后自动重连"不可靠**——实测过一次整夜合盖,醒来三个会话都卡在"connected 但传输已断",不会自愈,要手动 `cmux workspace reconnect --workspace `(详见 §3.12)。远端会话本身全部存活,救回零损失。命令行里有 `--persistent-lease-port` 参数暗示有租约机制,TTL 未知,但这次实测过夜(约 9 小时)远端会话没被回收。 --- ## 5. 已确认的实现与文档不一致 **`workspaceInheritWorkingDirectory: false` 不会用 ghostty 的 `working-directory`。** schema 文档原文是"When false, new workspaces use Ghostty's working-directory setting instead",但实测(含**重启 app 后复测**)新工作区一律落在家目录 `~`,无论 ghostty 那边设成什么。排查过程中排除了两个假设: - 不是"启动时缓存"——重启后行为不变 - 不是派生配置被剥离——cmux 在 `~/Library/Application Support/com.cmuxterm.app/config.ghostty` 生成的派生文件是**空的**,说明它直接读源配置渲染 而 `ghostty +show-config` 能正确回显 `working-directory`,所以问题在 cmux 侧。 **结论与处理**:`workspaceInheritWorkingDirectory` 保持默认 `true`(继承当前工作区目录)。设 `false` 会掉到家目录,比继承更难用。 要"固定从某个根目录开新工作区",用 `commands` 预设显式传 `--cwd`,这条路是可靠的: ```json { "name": "新工作区 · 99_code", "keywords": ["new", "workspace", "99code"], "command": "cmux workspace create --cwd ~/Documents/_work/99_code" } ``` 于是两种行为都有:`⌘N` 继承当前目录,`⌘⇧P` 搜预设从固定根目录开。 > 未验证的细节:上述测试都走 CLI `cmux workspace create`。UI 的 `⌘N` 路径是否也忽略该设置没单独确认过。 --- ## 6. 没配的东西 - **`actions` / `ui.newWorkspace`(+ 按钮菜单、自定义工作区布局)**:官方文档标注为 nightly 特性,稳定版可能不认。而且 `ui.newWorkspace.contextMenu` 一旦定义会**替换**默认菜单,如果解析失败会把 + 按钮菜单搞坏。风险不值当,等切 nightly 再说。 - **整个 `workspaceGroups` 块**:2026-08-05 起不用侧栏分组(原因见 §2),所以图标/颜色/落位规则和那条 `byCwd.*.contextMenu`(分组 + 按钮的右键菜单)都不配了。后者本来也有风险 —— schema 与上面那个 nightly 的 `ui.newWorkspace.contextMenu` 相同,一样会替换默认菜单。 - **自定义侧栏**:`~/.config/cmux/sidebars/*.swift`,运行时解释的 SwiftUI(beta)。`cmux docs sidebars` 有说明。 - **`cmux vm` 云端开发机**:需要 `cmux auth login`,是 cmux 自家的云环境,跟自建 SSH 机器是两套东西。 - **全局 SSH 连接复用**:`~/.ssh/config` 里给 `Host *` 加 `ControlMaster auto` 能让所有 ssh/scp/git 受益,不只 cmux 拖拽。影响面大所以没动。 --- ## 推理并行:DP、TP、PP、EP 与 CP 怎么选 > Source: https://weigao.cc/ai-systems/llm-inference/inference-parallelism/ > Date: 2026-07-28 > Tags: llm-inference, parallelism, tensor-parallel, pipeline-parallel, expert-parallel, context-parallel 推理并行不是“GPU 越多越快”,而是在多个 rank 之间重新分配权重、激活、KV、Expert token 和请求。每一种切分都会减少一部分单卡压力,同时增加新的通信、同步或空泡。 :::important[30 秒复习] - **一句话**:先说清要解决容量、单请求延迟还是集群吞吐,再按拓扑选择 DP、TP、PP、EP 或 CP。 - **三个判断**:DP 复制模型换吞吐;TP/EP 在层内切计算但频繁通信;PP/CP 分别沿层和上下文切分,适合不同容量与长序列约束。 - **核心模型**:`每 rank 时间 ≈ 本地计算 + 关键路径通信 + 同步/空泡`,并行只在减少项大于新增项时带来性能收益。 - **边界**:本文给出稳定的选择坐标,不给脱离模型、Batch、序列和互联拓扑的“最佳并行度”。 ::: ## 1. 先分清三个目标 并行配置通常在解决三类不同问题: | 目标 | 需要改善什么 | 常见起点 | |---|---|---| | **模型或 KV 放不下** | 每 rank 容量 | TP、PP、EP、CP,或先量化 | | **单请求太慢** | 关键路径计算时间 | 同节点 TP,前提是通信足够快 | | **集群吞吐不足** | 同时服务的请求数 | DP / replica,并配合负载均衡 | “模型能放下”只是可行性,不等于配置高效。先做[模拟器显存账本](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/),再用真实 Case 测 TTFT、TPOT、吞吐和通信占比。 ## 2. 五种并行各切什么 ### 2.1 Data Parallel:切请求 DP 让每个 replica 持有完整模型,把不同请求交给不同 replica: ```text Replica 0: 完整模型 ← 请求 A、C Replica 1: 完整模型 ← 请求 B、D ``` - **减少**:单 replica 的请求压力; - **增加**:模型副本占用和路由复杂度; - **适合**:模型单副本能放下,希望扩展总吞吐; - **不直接改善**:单请求关键路径。 在线服务常把 DP 与请求路由、Prefix 亲和性和弹性伸缩一起设计。若只看 GPU 数而忽略流量分布,可能出现一个 replica 排队、另一个空闲。 ### 2.2 Tensor Parallel:切层内张量 TP 把 Linear / Attention 等层内矩阵分到多个 rank,各 rank 计算局部结果,再通过 collective 合并: ```text W = [W0 | W1] Y0 = XW0 Y1 = XW1 Y = combine(Y0, Y1) ``` - **减少**:每 rank 的权重、部分计算和部分临时张量; - **增加**:几乎每层都出现 AllReduce / AllGather / ReduceScatter; - **适合**:高速互联域内解决容量或降低单请求计算时间; - **风险**:Batch 太小、跨慢链路或 TP 过大时,通信吞掉计算收益。 TP 的收益必须按目标拓扑测试。逻辑上的 `TP=8` 不说明 8 个 rank 是否在同一 NVLink/NVSwitch 域。 ### 2.3 Pipeline Parallel:切层 PP 把连续层段放到不同 stage: ```text Stage 0: Layer 0..N → activation Stage 1: Layer N+1..M ``` - **减少**:每 rank 的层数和权重容量; - **增加**:stage 边界传输、流水线空泡和调度复杂度; - **适合**:需要跨较慢链路扩展容量,或 TP 域已经用尽; - **风险**:在线推理的动态 Batch 和不等长请求让流水线更难填满。 PP 的通信频率低于逐层 TP,但单请求必须顺序经过各 stage。它更像容量与拓扑工具,不应默认视为降延迟工具。 ### 2.4 Expert Parallel:切 Expert EP 把 MoE Expert 分散到多个 rank。每层 Router 之后执行: ```text dispatch token → remote/local experts → combine result ``` - **减少**:每 rank 常驻的 Expert 权重; - **增加**:All-to-All、负载不均和 padding/drop 策略; - **适合**:Expert 总权重很大、每 token 只激活少量 Expert; - **风险**:热点 Expert、跨节点放置和小消息会放大通信。 MoE 的参数作用域、Router 和 dispatch/combine 由 [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/)负责;本页只把 EP 放回全局并行坐标。 ### 2.5 Context Parallel:切序列 CP 沿序列维分担长上下文计算或状态: - **减少**:单 rank 承担的序列计算、激活或部分 KV 压力; - **增加**:Attention 所需的 K/V 交换、归约或环形通信; - **适合**:单请求上下文超长,序列维本身成为容量或计算约束; - **风险**:通信模式与具体 Attention backend 强相关。 CP 不是 Prefix Cache,也不是把不同请求分给不同 GPU。它切的是一个请求内部的上下文维度。 ## 3. 作用域:global 不能直接当 per-rank 并行推理最常见的建模错误,是把全局量直接填进单 rank 公式。 | 量 | 常见作用域 | 需要检查 | |---|---|---| | Dense 权重 | TP/PP 后部分分摊 | 是否有复制层、LM Head、embedding | | Expert 权重 | EP/TP/PP 组合分摊 | shared expert 是否复制 | | KV Cache | 取决于 Attention 与 TP/CP 布局 | KV heads 是否真实切分 | | 请求数 / Batch | 可能是 replica、engine 或 global | 调度器口径 | | 通信量 | 每 collective / 每 rank / 全局总量 | 算法和拓扑 | | 吞吐 | 每 replica / 每 engine / 集群 | 是否包含排队和失败 | 任何容量或吞吐结论都应携带: ```text model × dtype × TP × PP × EP × CP × DP hardware/topology × ISL/OSL × concurrency ``` 详见[模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/#8-per-rank-vs-globalcluster)。 ## 4. 组合时先尊重拓扑 一个常见但不是普遍最优的组合原则是: 1. **高速域内**放通信频繁的 TP; 2. **按 Expert 放置**设计 EP,尽量控制 All-to-All 跨域; 3. **跨较慢链路**优先考虑通信频率较低的 PP 或副本级 DP; 4. **超长上下文**再评估 CP 是否比增加 KV 容量更合适。 这只是设计起点。实际系统还受到: - NUMA / PCIe 根复杂度; - NIC 数量和 GPU-NIC 亲和性; - collective 算法; - 通信-计算重叠; - Prefill 与 Decode 的不同消息大小; - 调度器是否能保持各 rank 工作一致。 [GPU Communication](https://weigao.cc/ai-systems/gpu-computing/gpu_communication/)负责互联与 collective 基础;拓扑 profile 必须对应真实机器,不能把另一种 NIC 布局的假设直接复用。 ## 5. 一个选择顺序 ```text 模型和目标 Batch 能否单卡放下? ├─ 能 │ ├─ 单请求延迟优先 → 测 TP=1/2/...,直到通信开始主导 │ └─ 集群吞吐优先 → 优先 DP / replica └─ 不能 ├─ Dense 权重主导 → 先量化,再在高速域内 TP;必要时 PP ├─ Expert 权重主导 → EP + 必要的 TP/PP ├─ KV/长上下文主导 → KV 压缩/量化/Paged KV,再评估 CP └─ 多项同时主导 → 建模各作用域后搜索组合 ``` 不要跳过“先量化/压缩是否更简单”这一问。增加 rank 会同时增加成本、故障面和通信,容量问题未必应该优先用分布式解决。 ## 6. 怎么验证并行配置 ### 服务指标 - TTFT、TPOT / ITL; - 请求与 token 吞吐; - P50 / P95 / P99; - 稳态可承载并发; - OOM、抢占和错误率。 ### 执行证据 - 每 rank 权重、KV、workspace; - collective 的次数、字节和关键路径时间; - rank 间计算/通信不平衡; - pipeline bubble; - Expert 负载与 token dispatch 分布; - CPU launch、同步和调度等待。 ### 最小实验 固定模型、硬件、拓扑、输入/输出分布和精度,只改变一个并行轴。至少比较: ```text per-rank memory critical-path latency delivered throughput communication share ``` 如果吞吐增加只来自更多副本,应报告扩容效率;不要写成“单实例加速”。 ## 7. 常见误区 - **TP 翻倍,延迟就减半**:collective、同步和小矩阵效率会限制收益。 - **PP 通信少,所以一定更快**:空泡和逐 stage 关键路径可能更重要。 - **EP 只影响显存**:dispatch/combine 和负载不均常在关键路径。 - **global Batch 可以直接代入 per-rank 显存**:调度和复制口径可能不同。 - **跨节点 TP 绝对不行**:应以目标网络和 Case 测量;但更慢、更不稳定的链路必须被显式建模。 - **卡越多容量越大,吞吐必然更高**:可行容量、理论容量和校准容量是三件事。 ## 相关页面 - [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/) — EP、Expert 参数和 dispatch/combine - [Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) — collective 之外的执行开销 - [推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) — 各层如何组装为 Serving Stack - [GPU Communication](https://weigao.cc/ai-systems/gpu-computing/gpu_communication/) — NVLink、NCCL、RDMA 与拓扑 - [Megatron 并行](https://weigao.cc/ai-systems/distributed-training/megatron_parallel/) — 更完整的训练侧并行原理 --- ## 推理 Kernel / Runtime 优化:少搬、少启、少等 > Source: https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/ > Date: 2026-07-28 > Tags: llm-inference, gpu-kernel, flashattention, flashdecode, kernel-fusion, cuda-graph Kernel 优化的技术名很多,但它们主要在消除四种浪费:不必要的 HBM 往返、并行度不足、过多 launch,以及 CPU/GPU 或 GPU/GPU 同步。先识别浪费类型,比先选 FlashAttention、Fusion 或 CUDA Graph 更可靠。 :::important[30 秒复习] - **一句话**:Kernel / Runtime 优化的统一目标是少搬数据、让并行度匹配形状、少启动 Kernel、少在关键路径同步。 - **三个判断**:Prefill 与 Decode 的 Attention 形状不同;Fusion/Graph 主要解决 HBM 与 launch,并不减少模型语义工作;局部加速受端到端占比和瓶颈迁移限制。 - **核心模型**:`T_step ≈ max(T_compute, T_memory) + T_launch + T_sync + T_comm`,优化必须指出降低了哪一项。 - **边界**:具体 backend、支持算子和性能随硬件、模型形状与版本变化,本文不维护框架功能支持表。 ::: ## 1. 四种浪费 | 浪费 | 典型证据 | 常见控制点 | |---|---|---| | **HBM 往返** | memory stall、高字节/FLOP、中间张量落 HBM | tiling、online reduction、fusion、低精度 | | **并行度不足** | SM 空闲、小 Q / 小 Batch / 小 shape | split-K/V、persistent kernel、批量化 | | **Launch 过多** | GPU 间有小空隙、CPU launch 在关键路径 | fusion、CUDA Graph、批量调度 | | **同步等待** | `cudaDeviceSynchronize`、D2H、collective barrier | 异步数据流、消除 host round-trip、重叠 | 这四项可能同时存在。Trace 用来定位时间,Roofline 用来判断算力/带宽,源码用来确认同步和数据依赖;单一指标不足以完成归因。 ## 2. Attention:Prefill 与 Decode 不是同一个形状 ### 2.1 Prefill 的 IO-aware Attention 朴素 Attention 会构造或反复访问较大的 score 中间量。IO-aware 实现通过分块和在线归约,让局部 Q/K/V 与 softmax 状态尽量留在片上存储: ```text 加载 Q tile → 流式加载 K/V tile → 更新局部 score、max、sum、output → 只写最终输出 ``` 关键收益不是改变 Attention 数学定义,而是减少中间结果的 HBM 读写。序列、head dimension、mask、dtype 和硬件都会影响实际 tile 与 backend。 [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/)负责模型语义;这里讨论同一语义如何更高效执行。 ### 2.2 Decode 的小 Q 与长 KV 单步 Decode 的 Q 很小,却要读取历史 KV。若只沿 Q 维并行,GPU 可能没有足够工作。常见方法是沿 KV 序列切分: ```text KV split 0 ─┐ KV split 1 ─┼→ partial attention → online reduce → output KV split N ─┘ ``` 它以额外的 partial result 与归约换取更高并行度。是否值得取决于上下文长度、Batch、KV 布局、head 数和 backend;“使用了 FlashDecode”本身不是收益证明。 ### 2.3 KV 布局会反向约束 Kernel Paged KV、Prefix 共享、KV 量化和 MLA 都会改变: - 地址是否连续; - 每 token / block 的字节; - Scale 或元数据读取; - head / latent 维度; - gather 和 dequant 是否能融合。 因此模型、内存管理和 Kernel 不是三个独立开关。KV 语义与生命周期见 [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/),MLA 见 [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/)。 ## 3. Fusion:减少中间数据和 launch 未融合的执行路径可能是: ```text Norm → 写 HBM → 读 HBM → Linear → 写 HBM → 读 HBM → Activation → 写 HBM ``` 若形状、数据依赖和资源允许,融合 Kernel 可以把中间值留在寄存器或共享内存,并减少 launch: ```text [Norm + Linear + Activation] → 一次或更少 HBM 往返 ``` ### Fusion 的代价 - 寄存器或共享内存压力可能降低 occupancy; - 组合数量增加,编译与维护成本上升; - 动态 shape、分支或稀有模型结构难以覆盖; - 一个大 Kernel 更难定位局部回归; - 通信或调度仍可能主导端到端时间。 所以判断标准不是“融合越多越好”,而是它是否降低关键路径上的字节与 launch,且没有把新瓶颈推到资源占用或编译系统。 ## 4. CUDA Graph:降低稳定路径的 CPU 开销 普通 eager 执行由 CPU 持续发起 Kernel。若每步 Kernel 很短,CPU launch、Python/C++ runtime 和同步间隙可能可见。CUDA Graph 捕获一条稳定执行路径,之后用较少 host 操作重放。 适合: - shape 和内存地址相对稳定; - Decode 重复执行相似图; - CPU launch 已在关键路径; - 框架能管理多种 Batch / token bucket 的 graph。 不适合直接假设有效的情况: - shape 高度动态; - 控制流或 backend 经常变化; - capture 需要大量 padding; - 图管理占用过多内存; - 真正瓶颈仍是 HBM 或通信。 Graph 优化的是运行时发射方式,不会减少模型参数或 Attention 的语义计算。 ## 5. Compile 与专用 Kernel 怎么放进地图 编译器和手写 Kernel 都在尝试把模型图映射为更好的执行计划,但控制点不同: | 路径 | 擅长 | 主要成本 | |---|---|---| | 图编译 / codegen | 跨 op fusion、常量折叠、布局与调度搜索 | 编译时间、shape specialization、fallback | | 模板化 Kernel | 覆盖常见模型形状,迭代快 | 模板边界与版本组合 | | 手写专用 Kernel | 对关键 shape/硬件做极致优化 | 开发、验证和可移植性 | | Vendor library | 稳定且覆盖常见算子 | 黑盒边界、版本与形状适配 | Serving 框架可能同时使用多条路径。框架名不能直接推出每个请求实际走了哪个 Kernel;应以构建日志、运行时选择和 Trace 为准。 ## 6. 一个诊断顺序 ### 6.1 确认端到端目标 - TTFT 慢:先拆 Queue、Prefill、首轮通信和采样; - TPOT / ITL 慢:拆权重/KV 读取、Decode Attention、通信和 launch; - 吞吐低:同时看有效 Batch、调度空洞和每步成本; - 尾延迟高:检查动态 shape、抢占、长 Prefill、编译 fallback。 ### 6.2 找 Top 时间块 Trace 中按时间和调用频率排序,但不要只看最慢单次 Kernel: ```text 贡献 = 单次时间 × 调用次数 × 关键路径重叠关系 ``` 一个 20 μs Kernel 调用数万次,可能比一个 2 ms 的偶发 Kernel 更值得优化。 ### 6.3 判断是哪类浪费 - Roofline / bytes → HBM 还是 compute; - shape / occupancy → 并行度是否足够; - CPU-GPU timeline → launch gap; - sync / memcpy / collective → 等待是否可消除或重叠; - backend 日志 / 源码 → 实际执行路径。 ### 6.4 做最小 A/B 固定模型、Case、精度、并行和调度,只改变一个 backend 或 runtime 控制点。记录: - 被优化时间块; - 端到端 TTFT / TPOT / throughput; - HBM、SM、launch、sync; - 正确性和稳定性; - 是否发生 fallback 或瓶颈迁移。 ## 7. Amdahl 边界 若被优化部分占端到端时间 `p`,局部加速 `s`,理想总体加速为: $$ S = \frac{1}{(1-p) + p/s} $$ 它提醒我们: - 只看 Kernel microbenchmark 会高估服务收益; - 优化后要重新采样,不能继续沿用旧占比; - 调度、通信或 Queue 不在同一 Kernel benchmark 内; - 局部时间与用户交付 token 的口径必须一致。 ## 8. 常见误区 - **FlashAttention 会让所有阶段都变快**:Prefill/Decode shape 与总占比不同。 - **Fusion 一定减少延迟**:资源压力、动态 shape 和 fallback 可能抵消收益。 - **CUDA Graph 是算力优化**:它主要减少 host launch 与运行时间隙。 - **低精度 checkpoint 证明走了低精度 Kernel**:必须检查实际 backend 和 Trace。 - **GPU Busy 高说明 Kernel 高效**:忙碌可以来自低效访存、同步或重复工作。 - **单 Kernel 快 2×,吞吐就快 2×**:端到端受 Amdahl 与新瓶颈限制。 ## 相关页面 - [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) — Roofline 判断 - [推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) — collective、拓扑与 rank 作用域 - [批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) — shape 和有效 Batch 从哪里来 - [Prefill Trace 解读](https://weigao.cc/ai-systems/llm-inference/prefill-trace-worker-dsa-mla/) — 时间线案例 - [推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) — Kernel 如何嵌入 Serving Stack --- ## KV Cache Hit Ratio 修正模型:从直觉到统一公式 > Source: https://weigao.cc/ai-systems/llm-inference/kv-cache-hit-ratio-tpm-correction-model/ > Date: 2026-07-28 > Tags: llm-inference, kv-cache, simulator, prefill, tpm KV prefix hit 同时减少重复计算、增加或复用 cache 读取,并可能改变瓶颈位置。端到端模型不能只把 TTFT 或 TPM 乘一个 `1-h`;它要把 compute、load、overlap、fixed 与 decode 分开。 :::important[30 秒复习] - **一句话**:Hit 的收益取决于省下的 prefill compute 能否覆盖 cache load;Delivered TPM 通过总 chip-time 传导,Physical TPM 还要修正计入的 token 分子。 - **三个判断**:Attention 与逐 token 线性算子缩放不同;load 必须按来源层级和 per-rank bytes 计;Delivered TPM 与 Physical TPM 的分子含义不同。 - **核心模型**:`TTFT = T_fixed + T_compute(h) + T_load(h) - α·min(T_compute,T_load)`,再与 `T_decode` 合成总 chip-time。 - **边界**:这是固定配置下的一阶敏感性/兼容模型,不替代目标 hit 下的原生仿真、显存重算与候选配置重排;输入缺失时应返回不可比较,而不是补零。 ::: ## 适用边界:固定配置分析,不替代原生重跑 本页适合解释单个既有配置在 hit 变化下的方向、兼容历史上缺少 KV 建模的结果,或作为 native 实现的对照近似。它不证明后处理与仿真可交换: $$ \operatorname{Correct}(\operatorname{Simulate}(h=0)) \ne \operatorname{Simulate}(h>0) $$ KV hit 会改变有效 token 数、算子 shape、MoE 通信 payload、显存与 Batch 约束、overlap 关系,并可能改变最优 TP/DP/EP/MoE-TP 配置。因此生产模拟器必须把 $h$ 作为场景输入,在模型层重新执行算子、通信、显存和候选配置搜索;编排层不应根据 breakdown 展示标签二次推断物理缩放。完整的参数归属与验收规则见[模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/#场景输入不是结果校准先做可交换性检查)。 ## 1. 本页接收什么、输出什么 输入至少包括: - 连续 prefix 的 token hit ratio $h$; - 0-hit prefill breakdown; - 命中 payload 的驻留层级与 per-rank bytes/token; - 有效传输带宽与 overlap 假设; - decode chip-time 及 ISL/OSL 口径。 输出是同一 workload identity、同一并行配置、同一可行性边界下的 TTFT 与 TPM 近似修正。它不能用于跨配置重排,也不能复用 0-hit 下已经失效的 Batch 或显存结论。若 $h$ 是“命中请求比例”而不是“命中 token 比例”,必须先转换,二者不能直接代入同一公式。 ## 2. Compute:按算子作用域缩放 ![KV Cache Hit 对 Prefill Causal Attention 计算面积的影响](https://weigao.cc/docs/ai-systems/llm-inference/images/kv_hit_attention_area.png) Dense causal interaction 的剩余区域近似为 $1-h^2$;完整几何推导及适用条件见 [Causal Attention 命中面积](https://weigao.cc/ai-systems/llm-inference/causal-attention-kv-hit-area/)。这里直接使用它,不再重复证明。 ![Attention vs FFN 计算量对比](https://weigao.cc/docs/ai-systems/llm-inference/images/kv_hit_attn_vs_ffn.png) 有可用 breakdown 时: $$ \begin{aligned} T_{\text{compute}}(h) &=T_{\text{attn-interact},0}(1-h^2)\\ &\quad+T_{\text{token-linear},0}(1-h)\\ &\quad+T_{\text{other},0}f_{\text{other}}(h) \end{aligned} $$ | Bucket | 一阶缩放 | 说明 | |---|---|---| | causal QK/AV interaction | $1-h^2$ | suffix 仍访问 cached prefix | | QKV/O projection、FFN/MoE | $1-h$ | 只为 miss suffix 重算 | | communication/runtime | 单独建模 | 可能固定、分段或随 batch 改变 | 若 trace 只给一个混合 “attention” bucket,不能确定其中 projection 与 interaction 的占比。此时应保留区间或校准系数,而不是假装分解精确。 ## 3. Load:命中不等于免费驻留 ![KV Cache Hit 对 Prefill TTFT 与 TPM 的影响](https://weigao.cc/docs/ai-systems/llm-inference/images/kv_hit_ttft_tpm.png) 若命中的 prefix 已在 GPU HBM,额外 transfer 可以接近零;若在 Host、NVMe 或远端 cache,需要加载到执行设备。令 $N_{\text{load}}$ 为本次确实迁移的 hit tokens: $$ T_{\text{load}}(h) =\frac{N_{\text{load}}(h)\times S_{\text{kv/token/rank}}} {B_{\text{effective/rank}}} $$ 简单场景可令 $N_{\text{load}}=hN$,但共享驻留、局部命中、分层 cache 或压缩传输都会改变它。 MHA/GQA 的 per-rank payload 可从 [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) 的全局逻辑公式出发,并按已确认的 TP layout 修正;MLA、CSA/HCA 必须使用各自 schema。不要把全模型 bytes 除以单卡带宽,也不要把理论链路峰值当有效带宽。 ## 4. Overlap 与 TTFT 令 $\alpha\in[0,1]$ 表示 load 与可变 compute 的可重叠程度: $$ T_{\text{overlap}}(h) =\alpha\min\!\left( T_{\text{load}}(h), T_{\text{compute}}(h) \right) $$ $$ \operatorname{TTFT}(h) =T_{\text{fixed}} +T_{\text{load}}(h) +T_{\text{compute}}(h) -T_{\text{overlap}}(h) $$ | $\alpha$ | 解释 | 可变部分 | |---:|---|---| | 0 | 不重叠的保守复刻 | $T_{\text{load}}+T_{\text{compute}}$ | | 1 | 充分重叠的上界 | $\max(T_{\text{load}},T_{\text{compute}})$ | $\alpha$ 不是硬件常数。它受 chunk、prefetch、scheduler、并发和依赖链影响,应由 trace 或实验校准;配置目标只能标为假设。 ## 5. 从 TTFT 到两种 TPM 设同一规范化请求的: $$ C(h)=P(h)+D $$ - $P(h)$:hit 修正后的 prefill chip-time。 - $D$:decode chip-time。 若 prefix hit 不改变输出长度与 decode 执行,$D$ 可暂视为固定;若 scheduler、cache contention 或 batch 形态同时变化,必须重测。 | 口径 | 公式 | 回答的问题 | |---|---|---| | Delivered TPM | $\frac{(\mathrm{ISL}+\mathrm{OSL})\times60000}{C(h)}$ | 单位 chip-time 向用户交付多少 token? | | Physical TPM | $\frac{(\mathrm{ISL}(1-h)+\mathrm{OSL})\times60000}{C(h)}$ | 按“新算 token”口径折算多少 token? | 单位要求 $C(h)$ 使用毫秒;若使用秒,分子应为 60。两种分子不可混用: - cached input 仍属于用户请求,所以 Delivered TPM 保留完整 ISL。 - Physical TPM 去掉未重新执行的 prefix token,但它只是 token 记账口径;suffix 对 cached prefix 的 attention 与 cache load 仍消耗硬件,不能把它当能耗或 FLOPs 真值。 高 OSL 或 decode 占比高时,prefill 即使显著缩短,端到端 TPM 增益也会被 $D$ 稀释。 ## 6. 数据不足时如何退化 若没有算子 breakdown,可用单线性近似: $$ T_{\text{compute}}(h)\approx(1-h)T_0 $$ 若进一步假设充分 overlap,并定义全量 cache load 与 0-hit compute 的时间比: $$ \delta=\frac{N S_{\text{kv/token/rank}}} {B_{\text{effective/rank}}T_0} $$ 则可写成趋势模型: $$ \Theta(h) \approx\frac{\Theta_0} {\max(1-h,\delta h)} $$ 它只用于说明“compute 下降、load 上升”的交叉点。相比完整模型,它没有显式固定开销、算子差异和 decode,不能用于承诺绝对容量或 SLO。 ## 7. 从 measured hit 修正到 target hit 若压测已经在 $h_m$ 下完成,而目标是 $h_t$,应使用同一校准模型分别求总 chip-time: $$ C(h)=P(h)+D $$ $$ \operatorname{TPM}^{t}_{\text{delivered}} =\operatorname{TPM}^{m}_{\text{delivered}} \times\frac{C(h_m)}{C(h_t)} $$ 再按目标 hit 转为 Physical TPM: $$ \operatorname{TPM}^{t}_{\text{physical}} =\operatorname{TPM}^{t}_{\text{delivered}} \times \frac{\mathrm{ISL}(1-h_t)+\mathrm{OSL}} {\mathrm{ISL}+\mathrm{OSL}} $$ 这个相对修正要求模型、ISL/OSL、并行配置、并发和 cache 层级一致。若这些条件变化,不能把 hit 差异当唯一自变量。 ## 8. 校准与误用检查 在输出结果前确认: 1. $h$ 是 token-weighted 连续 prefix hit,还是其他统计? 2. baseline 是 0-hit,还是已含 measured hit? 3. attention bucket 是否拆出 projection? 4. payload 是全局还是 per-rank,是否包含 Scale/对齐? 5. 命中数据已经在 HBM,还是需要 Host/远端加载? 6. 有效带宽和 $\alpha$ 来自实测还是配置假设? 7. 输出是 Delivered 还是 Physical TPM? 8. decode 是否确实可视为不变? 9. 当前用途是固定配置敏感性分析,还是需要重新搜索候选配置? 缺失项应明确标记 `missing`、`unavailable` 或 `not_comparable`。估算值与校准值也应分栏展示。 ## 相关页面 - [Causal Attention 命中面积](https://weigao.cc/ai-systems/llm-inference/causal-attention-kv-hit-area/) — `1-h²` 的唯一推导页。 - [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) — bytes/token、per-rank、分页与驻留生命周期。 - [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/) — latent KV 的 payload 与 kernel 边界。 - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 公式作用域、估算和校准合同。 --- ## MoE 推理:Expert Parallelism(EP)、显存与调度 > Source: https://weigao.cc/ai-systems/llm-inference/moe-inference/ > Date: 2026-07-28 > Tags: moe, expert-parallelism, inference MoE 推理的难点不只是“每个 token 只激活少量参数”。所有 expert 权重仍要被放置和管理,token 还要经过路由、跨 rank dispatch、expert 计算与 combine。 这篇负责 MoE 的数据流和参数口径;通用 TP/PP/DP/EP 组合由 [推理并行策略](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) 展开,显存与吞吐公式由 [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) 收口。 :::important[30 秒复习] - **一句话**:MoE 用稀疏激活降低单 token 计算,但把 Serving 复杂度转移到全量 expert 驻留、路由、all-to-all 与负载均衡。 - **三个判断**:active 参数决定每 token 计算而非总权重显存;EP 分散不同 expert 但不自动切开单个 expert;性能必须同时看 expert token 分布、dispatch/combine 与 grouped GEMM。 - **核心模型**:$y_t=\sum_{e\in\operatorname{TopK}(t)}p_{t,e}E_e(x_t)+E_{\text{shared}}(x_t)$,每 rank 显存包含 dense/attention shard、local experts、KV 与 runtime workspace。 - **边界**:expert 数、top-k、shared expert、路由函数和通信实现均为模型/版本合同;本文不把某个未核验模型参数或公开 benchmark 外推成通用结论。 ::: ![MoE 推理中的 Router、Dispatch、Grouped GEMM、Combine 和 Shared Expert 路径](https://weigao.cc/docs/ai-systems/llm-inference/images/moe-dispatch-compute-combine.svg) ## 1. Expert 与三种参数口径 一个常见 SwiGLU expert 包含 gate、up、down 三个投影: ```text gate/up: [hidden, expert_intermediate] down: [expert_intermediate, hidden] ``` Router 为每个 token 选择 top-k routed experts,并用路由权重合并输出;shared expert 若存在,则对所有 token 执行。 必须区分: | 口径 | 含义 | 主要用途 | |------|------|----------| | 总参数 | dense/shared + 全部 routed experts | checkpoint 与全局权重容量 | | Active 参数 | 单 token 实际经过的 dense/shared + top-k experts | 计算量近似 | | 单 expert 参数 | 一个 expert 的权重 | 判断 expert 是否需要内部 TP | “active 40B”不等于模型只需保存 40B 参数,也不等于 40GB 显存。权重字节还取决于存储格式、scale、padding 和 runtime layout。 ## 2. 完整执行流程:Dispatch - Compute - Combine ```text hidden states -> Router: top-k expert ids + weights -> Dispatch #1: token 按 expert owner 重排/发送 -> Grouped GEMM: 各 expert 对收到的 token 分别执行 FFN -> Combine #2: expert 输出回到原 token/rank -> Weighted sum: 按 router weights 加权聚合 -> + shared expert path(若模型定义) ``` 对 token $t$: $$ y_t = \sum_{e\in\operatorname{TopK}(t)} p_{t,e}E_e(x_t) +E_{\text{shared}}(x_t) $$ Router 对每个 token 的当前 hidden state $x_t$ 独立打分;它不直接读取 KV Cache 或其他 token。由于 $x_t$ 已经过 Attention,它仍然包含上下文信息。每个 routed expert $E_e$ 是一套独立 FFN 参数:它只对收到的向量执行 MLP,不需要历史 K/V,也不直接读取其他 token。 实际 kernel 通常不会一次只算一个向量,而是把路由到同一 expert 的 token 收拢成子 batch,再用 Grouped GEMM 执行。这会让性能依赖 `tokens_per_expert`、padding 和负载均衡,但不会改变 Expert FFN 的逐 token 语义。 在 Expert 跨 rank 放置的 EP 配置中,dispatch 通常构成第一次 all-to-all,返回 expert 输出的 combine 构成第二次 all-to-all。若 top-k expert 位于本地、模型运行在单卡,或 backend 使用不同的 fused/point-to-point 实现,则不能仅凭“这是 MoE”断言一定发生两次跨卡 collective。 这条路径的关键观测不是“有多少 expert”,而是: ```text tokens_per_expert distribution dispatch bytes / duration grouped GEMM shape / duration combine bytes / duration load imbalance and synchronization ``` 若少数 expert 过热,慢 rank 会延长整个 collective 的完成时间;若 batch/chunk 太小,每个 expert 收到的 token 太少,grouped GEMM 也可能低效。 ## 3. 并行切分策略 MoE 页只保留与 expert 数据流直接相关的边界: - **EP(Expert Parallel)**:不同 rank 持有不同 experts;token 通过 all-to-all 到 owner,再把结果送回; - **TP(Tensor Parallel)**:切一个过大的 expert 或 dense/attention 矩阵;会增加该层内部 collective; - **DP**:复制可服务的模型实例、分摊请求;通常不降低单实例权重显存; - **PP**:按层切 stage;降低每个 stage 的层数,同时引入流水与激活传输。 EP 的简化权重估算是: $$ M_{\text{weights,rank}} \approx M_{\text{dense/attn shard}} +N_{\text{local experts}}S_{\text{expert}} +M_{\text{shared expert}} $$ 其中: $$ N_{\text{local experts}} \approx \frac{N_{\text{routed experts}}}{N_{\text{EP}}} $$ 这只在 expert 均匀放置且没有 replica/冗余时成立。若单个 expert 在保留 KV 与 workspace 后仍放不进一张卡,简单 EP 不够,需要 expert 内 TP、PP 或其他切分。 完整并行选择、通信和拓扑合同见 [推理并行策略](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/)。 ## 4. 单 rank 显存合同 不要只把总参数除以 GPU 数。每 rank 的常驻与峰值显存至少包括: $$ \begin{aligned} M_{\text{rank}} \approx{}& M_{\text{dense/attn shard}} +M_{\text{local expert weights}} \\ &+M_{\text{KV/state}} +M_{\text{activation}} +M_{\text{MoE workspace}} \\ &+M_{\text{communication buffers}} +M_{\text{runtime reserve}} \end{aligned} $$ 权重部分要使用实际 resident layout: $$ M_{\text{weight}} = N_{\text{params}}\times b_{\text{payload/param}} +M_{\text{scale}} +M_{\text{metadata/padding}} $$ Active 参数适合估算计算,不适合代替 resident weights。量化格式的 scale 粒度与模拟器实现见 [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) 和 [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/)。 ## 5. Residency、Offload 与通信 :::details[查证层:工程实现需要额外确认什么] 低延迟 Serving 通常倾向让 local expert 权重常驻 HBM,因为按 token 或按层从 CPU/NVMe 取权重会引入带宽、排队和 miss penalty。但“必须全部常驻”不是脱离 SLO 的定律:离线、低并发或分层缓存场景可能接受 offload。 评估 expert cache / offload 时应记录: ```text resident experts, hit/miss rate, transfer bytes, PCIe/RDMA/NVMe effective bandwidth, load latency, overlap ratio, TTFT/TPOT impact ``` [DeepEP](https://github.com/deepseek-ai/DeepEP) 等库提供 MoE dispatch/combine 通信能力。吞吐、低延迟模式、精度路径与 overlap 能力随目标版本和拓扑变化,不能把单个公开案例的 EP 规模或倍数当成容量常量。 一个粗略的 activation 通信量起点是: $$ V_{\text{dispatch}} \propto N_{\text{tokens}}\times k\times d_{\text{hidden}}\times b_{\text{act}} $$ Combine 还有返回流量。真实 per-rank 字节取决于 token 是否本地命中、路由分布、量化/压缩、冗余发送和 collective 实现,需用 trace 或通信计数器校准。 ::: ## 6. 从 Trace 判断 MoE 瓶颈 按以下顺序收集证据: 1. Router 输出的 `tokens_per_expert` 是否倾斜; 2. Dispatch/combine 是否处于 GPU 关键路径; 3. Grouped GEMM 的每 expert $M$ 维是否过小; 4. 慢 rank、网络链路或同步是否拖长 collective; 5. Prefill chunk / Decode batch 的变化是否同时改变以上形状。 仅看到 NCCL 时间高,不能判断是网络带宽不足:上游计算迟到、负载不均或显式同步也会让 collective 包络变长。 ## 相关页面 - [Token Flow 与 Hidden State](https://weigao.cc/ai-systems/llm-inference/token-flow-hidden-state/) - [推理并行策略](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) - [Serving Stack 与框架选型](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) --- ## 模拟器建模指南:显存与吞吐公式 > Source: https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/ > Date: 2026-07-28 > Tags: simulator, memory-modeling, inference 模拟器的价值不是给出一个看似精确的数字,而是把**理论估计、部署配置、实测校准和未知项**分开。只要 scope 或 resident layout 混淆,显存和吞吐结论都可能差一个并行度或格式开销。 本文定义最小建模合同。模型特有结构应作为插件参数进入,不在通用公式里硬编码。 :::important[30 秒复习] - **一句话**:先按 per-rank 资源做可行性上界,再用相同硬件、软件栈与 workload 的校准表估计可达容量。 - **三个判断**:weights/KV/activation/workspace 的 scope 必须分开;checkpoint dtype 不等于 executed kernel 或 resident layout;缺失、不可用与不可比较都不能当作零。 - **核心模型**:$M_{\text{rank}}=M_W+M_{KV}+M_A+M_{\text{workspace}}+M_{\text{comm}}+M_{\text{reserve}}$,$T_{\text{step}}\approx\max(T_{\text{compute}},T_{\text{memory}},T_{\text{comm,unhidden}})+O_{\text{runtime}}$。 - **边界**:公式给出估计与敏感性,不替代 engine 显存实测、kernel trace、网络校准和 SLO 压测。 ::: ## 1. 建模对象与状态 | 组成 | 生命周期 | 主要 scope | 首选证据 | |------|----------|------------|----------| | Weights | 常驻 | per-rank,受 TP/PP/EP/replica 影响 | loader 日志 + resident memory | | KV / recurrent state | 随请求增长 | per-rank、per-request、per-block | allocator / block table | | Activations | 单次 forward 瞬态 | per-rank、per-step | peak allocation / trace | | Kernel workspace | 算子或 graph 生命周期 | per-rank、shape-dependent | backend 实测 | | Communication buffers | collective / connector | per-rank、拓扑相关 | runtime 配置 + 实测 | | Runtime reserve | allocator、graph、碎片 | per-rank | 启动后与压力下水位 | 每个输出还应带状态:`measured`(当前 identity 实测)、`estimated`(公式估算)、`missing`(输入缺失)、`unavailable`(能力无法提供)或 `not_comparable`(identity 不同)。 ## 场景输入不是结果校准:先做可交换性检查 最容易犯的建模错误,是把**改变系统工作量的场景输入**当成**结果出来后的校准参数**。后处理通常更快,也容易被 API、缓存和持久化层统一接入;但如果参数会改变算子输入、执行图、资源约束或配置优先级,后处理只能形成近似,不能替代原生仿真。 令 $S(c,h)$ 表示在配置 $c$ 和场景参数 $h$ 下执行仿真,$C_h$ 表示对 0 场景结果做后处理。进入架构设计前,先检查: $$ C_h(S(c,0))\stackrel{?}{=}S(c,h) $$ 如果不相等,这个变换就不与仿真可交换。若系统还会搜索 TP/DP/EP/MoE-TP、Batch 或 placement,则还要检查: $$ \arg\max_c C_h(S(c,0)) \stackrel{?}{=} \arg\max_c S(c,h) $$ 第二式不成立时,即使某个固定配置的 TTFT 修正看起来合理,也不能据此声称找到了目标场景的最优配置。 ### 四个问题决定参数归属 | 问题 | 若答案为“会” | 归属 | |---|---|---| | 会改变算子输入 shape、有效 token 数或执行次数吗? | GEMM、Attention、MoE 等工作量改变 | 模型层 | | 会改变执行图、通信 payload 或 overlap 依赖吗? | critical path 与瓶颈可能迁移 | 模型层 | | 会改变显存、Batch 上限或其他可行性约束吗? | 候选集合本身改变 | 模型层 | | 会改变候选配置的排序吗? | 必须重新执行配置搜索 | 模型层 | 只有不改变上述物理语义、只影响参数校验、任务编排、缓存身份、并发保护、持久化、版本、provenance 或结果表达的能力,才应留在编排层。 ### KV hit 是典型反例 Prefix KV hit ratio $h$ 不只是“把 TTFT 乘一个系数”。它至少可能改变: - Attention 的 causal interaction 区域; - QKV/O projection、Dense FFN 和 MoE 的 miss token 数; - MoE dispatch/combine 与其他 token-dependent communication 的 payload; - KV load/transfer、显存占用、可用 Batch 与 admission; - TP/DP/EP/MoE-TP 等候选配置的排序; - compute、communication 与 transfer 的 overlap 关系。 因此,prefix、miss tokens、算子计算、通信量、显存和候选配置搜索应由模型层统一计算。编排层可以透传 KV/transfer/overlap 策略并管理结果,但不应根据 `Attention`、`Communication`、`MoE` 等展示标签猜测物理缩放规则。后处理模型仍可用于固定配置的敏感性分析、历史结果兼容和 native 实现的对照,但必须标为 `estimated`,且不能代替目标场景重跑。 ### 验收必须覆盖物理不变量 缓存、持久化、数值比例和 API 闭环只能证明结果被正确编排,不能证明模型物理正确。至少增加以下断言: 1. KV hit 上升时,只由 miss tokens 驱动的 MoE dispatch/combine payload 单调下降; 2. 当 $h\to1$ 且 miss tokens 趋近于零时,token-dependent communication 趋近于零,只保留 launch、sync 等固定开销; 3. 在可解析的简单模型和固定配置上,native 仿真与后处理近似应在声明误差内一致; 4. workload 改变后必须重新评价候选配置,并允许排序发生变化; 5. 模型层输出算子、通信、显存和 overlap 证据;编排层只校验、缓存、持久化和展示这些证据。 可复用的判断规则是: > 改变“系统看到的工作量”的参数放模型层;只改变“结果如何存取和表达”的能力放编排层。 ## 2. Weights Memory [per-rank] 通用权重公式是: $$ M_{W,\text{rank}}=\sum_j\left(N_{j,\text{local}}b_{j,\text{payload}}+M_{j,\text{scale}}+M_{j,\text{metadata/padding}}\right) $$ 其中 `local` 必须由真实 placement 得到。不能简单把总参数除以 `tp × ep × pp`: - TP 只切其负责的矩阵; - EP 只分散 routed experts,shared/dense 通常另有切法; - PP 按 layer/stage 分配,不一定均匀; - tied embedding、expert replica、padding 与冗余会改变结果。 ### FP4 的 scale 不能混写 4-bit payload 的逻辑下界约为: $$ M_{\text{payload}}=\left\lceil N_{\text{params}}/2\right\rceil\text{ bytes} $$ 但两种常见配方的 block scale 不同: | 格式 | Block | Scale 类型 | 逻辑 block-scale 开销 | |------|-------|------------|------------------------| | NVFP4 | 16 values | E4M3 | $\lceil N/16\rceil\times1$ byte | | MXFP4 | 32 values | E8M0 | $\lceil N/32\rceil\times1$ byte | NVFP4 还可能包含更高精度的 tensor-level scale;两者都可能因 tile 对齐、packing、padding 或 runtime 转换产生额外 resident bytes。不要再用一个通用 `fp4 -> per-32 E8M0` 分支同时代表 NVFP4 与 MXFP4;逻辑字节用于估算,容量判断优先使用实际 resident bytes。 ## 3. KV / State Memory [per-rank, per-request] 对标准 KV Cache,可从每 token、每本地层的字节开始: $$ m_{\text{KV/token/layer/rank}}=2N_{\text{KV heads,local}}d_{\text{head}}b_{\text{KV}} $$ 再叠加本地层数、请求和 block 分配: $$ M_{\text{KV,rank}}=\sum_rN_{\text{layers,local}}N_{\text{tokens,allocated}}(r)m_{\text{KV/token/layer/rank}}+M_{\text{metadata}} $$ Paged KV 使用已分配 block,而不是只用有效 token: $$ N_{\text{tokens,allocated}}=\left\lceil N_{\text{context}}/B_{\text{block}}\right\rceil B_{\text{block}} $$ MLA、稀疏/压缩 Attention、hybrid recurrent state 不能套同一个 head 公式;应提供 backend-specific layout,并分别标记 payload、index、state、load/transfer buffer。具体结构见 [CSA/HCA 注意力](https://weigao.cc/ai-systems/llm-inference/csa-hca-attention/) 和 [GDN 与 Chunked Prefill](https://weigao.cc/ai-systems/llm-inference/gdn-chunked-prefill/)。 ## 4. Activation / Workspace Memory [per-rank] Activation 峰值随本轮 shape 变化,常出现在较大的 Prefill chunk,但这不是无需测量的定律。接口应接收 phase、scheduled tokens、hidden/intermediate dimensions、local heads/experts、dtype、kernel/backend 与 graph mode。 一个用于敏感性分析的粗估是: $$ M_A\propto N_{\text{scheduled tokens}}d_{\text{hidden}}b_{\text{activation}}\alpha_{\text{model/backend}} $$ $\alpha$ 不是固定常数:融合会复用 buffer,编译图可能预留 workspace,MoE、spec decode 与结构化输出还会新增瞬态张量。峰值必须以 allocator snapshot 或目标 shape 的运行结果校准。 ## 5. MoE Workspace 与通信 MoE 需要分别建模 router/top-k metadata、dispatch/combine buffers、per-expert offsets/padding 与 grouped GEMM workspace。 Activation 发送量的起点可写为: $$ V_{\text{dispatch}}\propto N_{\text{tokens}}\times k\times d_{\text{hidden}}\times b_{\text{act}} $$ 但 local expert 命中、路由倾斜、容量/padding、低精度通信与双缓冲都会改变 per-rank 字节。通信时间也不能只写 `bytes / peak bandwidth`: $$ T_{\text{comm}}=L(\text{message,topology,ranks})+V/B_{\text{effective}} $$ 若与计算 overlap,模型只扣除未隐藏部分,并以 trace 校准 overlap ratio。MoE 数据流见 [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/)。 ## 6. 低精度运行时能力合同 Checkpoint 名称不等于运行时能力。能力矩阵的 identity 至少包含 checkpoint recipe、GPU architecture、engine/version、kernel backend、operand dtype 与 scale layout;输出包括 executed kernel、resident bytes、workspace、conversion/fallback、质量/性能状态和证据。 FP4 checkpoint 可能原生执行、运行时转换、回退到更高精度或不支持。只有加载日志、resident memory 和 kernel trace 一致时,才能把“checkpoint 格式”提升为“实际执行格式”。 ## 7. 吞吐、延迟与 Spec Decode 单步执行时间可以分解为: $$ T_{\text{step}}\approx\max(T_{\text{compute}},T_{\text{memory}},T_{\text{comm,overlapped}})+T_{\text{comm,unhidden}}+O_{\text{runtime}} $$ 端到端还要加入 queue、scheduler、KV load/transfer 与 sampling。Prefill 和 Decode 应使用不同 shape 与校准曲线。 Speculative Decoding / MTP 不能建模成固定倍数。至少需要 draft/extra-head weights、workspace、draft tokens per verify、accepted-token distribution、verify cost、extra KV/state 与 scheduler interaction。模型原生 MTP 与独立 draft model 不能共享同一开销公式。 ## 8. Per-rank vs Global/Cluster | 量 | Per-rank | Global / Cluster | |----|----------|------------------| | Weights | 当前 rank 实际 resident bytes | 同一 replica 各 shard 求和;cluster 还包含 DP replica 与冗余 | | KV / state | 当前 rank 的 pool 与占用 | 仅在 scope 一致时对 owner ranks 求和 | | Activation peak | 当前 rank、当前 step 的峰值 | 通常看所有 rank 的最大值;不拿求和判断单卡 OOM | | Throughput | rank/instance 口径需注明 | 可对独立 replica 求和;shard 内不能重复计 token | | Batch / capacity | 受最紧 rank 与 stage 约束 | 不是 `per-rank × GPU 数` 的通用关系 | 字段名最好直接编码 scope,例如 `weight_resident_bytes_per_rank`、`request_tokens_per_second_per_replica` 和 `cluster_slo_goodput`。 ## 9. 配置项 vs Calibration Table **配置项**描述部署计划:model/checkpoint、TP/EP/PP/DP/CP 与 placement、batch/concurrency、context/token budget、KV layout/dtype、weight recipe、engine/version/backend 与硬件拓扑。 **Calibration Table**描述精确 identity 下测到的 `kernel_time(shape)`、resident bytes、all-to-all latency、grouped GEMM efficiency、KV load/transfer latency 与 runtime reserve。 Calibration key 至少包含 model、hardware、engine version、kernel backend、precision recipe、parallelism 与 workload bucket。 Identity 不同的记录应是 `not_comparable`,而不是自动平均。 ## 10. 不能硬编码的结论 1. Prefill 恒为 compute-bound、Decode 恒为 memory-bound; 2. 峰值 FLOPS/HBM/互联带宽等于有效能力; 3. expert 路由均匀、collective 完全 overlap; 4. prefix hit 免费,或 KV fragmentation 固定; 5. FP4 checkpoint 必然以 FP4 kernel 执行; 6. 某个框架/硬件组合固定快多少倍; 7. 未提供的数据等于零。 模拟器应同时输出假设、敏感性与最紧约束,让读者能回答“数字为什么是这样”和“换哪个输入会改变结论”。 ## 相关页面 - [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/) - [推理并行策略](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) - [KV Cache Hit Ratio 修正模型](https://weigao.cc/ai-systems/llm-inference/kv-cache-hit-ratio-tpm-correction-model/) - [Chunked Prefill 深入分析](https://weigao.cc/ai-systems/llm-inference/chunked-prefill-deep-dive/) - [Serving Stack 与框架选型](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) --- ## Token Flow 与 Hidden State:从 Attention 到 LM Head > Source: https://weigao.cc/ai-systems/llm-inference/token-flow-hidden-state/ > Date: 2026-07-28 > Tags: llm-inference, transformer, hidden-state, attention, moe, lm-head 一次自回归生成可以沿着一条主线理解:token id 经 Embedding 变成 hidden state,Transformer blocks 不断更新这条向量,LM Head 再把它投影成词表 logits,Sampling 选出下一个 token。 :::important[30 秒复习] - **一句话**:模型内部流动的主体是 hidden state;Attention、FFN/MoE 更新它,LM Head 与 Sampling 才把它变回 token。 - **三个判断**:Attention 读取并聚合可见上下文;FFN/MoE 负责逐 token 变换;LM Head 给全词表打分但每步通常只选一个 token。 - **核心模型**:`token id → embedding → hidden state → Attention → FFN/MoE → LM Head → logits → Sampling → next token`。 - **边界**:这是单步数据流,不等于 serving 请求生命周期;张量形状、词表大小和 expert 数都由具体模型与批处理方式决定。 ::: ## 1. 一次 decode step 流过哪里 如下所示: ```text token id -> embedding lookup -> hidden state -> Attention(读历史 KV,写当前 KV) -> FFN or MoE -> final hidden state -> LM Head -> vocab logits -> Sampling -> next token id ``` 蓝色主线始终是 hidden state;KV、router 和 sampling 是围绕它发生的读写或决策。理解这条线后,Attention、MoE 和服务指标就不会被误当成彼此独立的模块。 ## 2. Hidden state 是什么 输入最初只是 token id: ```text input_ids: [B, S] ``` Embedding table 的形状是 `[vocab_size, hidden_size]`。每个 token id 查出一行后,输入变成: ```text hidden_states: [B, S, H] ``` * `B`:同时处理的序列数。 * `S`:本次 forward 的 token 数。 * `H`:模型的 hidden size。 Embedding 可以看作第 0 层 hidden state。此后每个 Transformer block 都接收并输出同样主维度的 tensor;“语义”不是一个单独字段,而是分布在向量各维及层间变换里。 Prefill 常处理 `[B, S, H]`,decode 每个序列每步通常只新增一个 token,因此逻辑形状接近 `[B, 1, H]`。实际 kernel 可能展平 batch/token 维或采用 packed layout,但不改变这条语义主线! ## 3. Attention 如何更新 hidden state Attention 不是直接选下一个词。它从当前 hidden state 生成 Query,并让 Query 读取可见上下文的 Key/Value: ```text current hidden state -> Q/K/V projection -> read historical KV cache -> attention output -> residual update -> contextualized hidden state ``` 在 decode 中,历史 K/V 已存入 [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/),当前 step 只生成并追加新 token 的 K/V;Query 仍需访问可见历史。MHA、GQA、MLA、稀疏与递推结构如何改变这一步,统一由 [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/) 解释。 ## 4. 一个 token 在一个 MoE 层里经历什么 以常见的 pre-norm decoder block 为例,省略具体 Norm 名称和张量 layout 后,一个 token 的核心路径是: ```text layer input hidden state -> Attention:读历史 K/V,写当前 token 的 K/V -> residual update -> Router:用当前 token 的 hidden state 计算 expert scores -> top-k expert ids + gate weights -> Dispatch #1:按 expert owner 发送 hidden state(跨 rank EP 时) -> Expert FFN:逐 token MLP,不读取 KV Cache -> Combine #2:expert 输出返回原 token/rank(跨 rank EP 时) -> 按 gate weights 加权求和 -> residual update -> layer output hidden state ``` 这里有四个容易混淆的边界: 1. **KV Cache 只属于 Attention 路径**:标准 Transformer 的 Expert FFN 不读取历史 K/V,也不产生自己的 KV。 2. **Router 的直接输入是当前 token 的 hidden state**:它不单独读取 KV Cache 或其他 token;但该 hidden state 已经过 Attention,因此已经携带上下文信息。 3. **Expert FFN 在模型语义上是逐 token 计算**:一个 expert 只对收到的向量做 MLP,不直接读取其他 token。运行时会把路由到同一 expert 的 token 打包成 Grouped GEMM,以提高 GPU 利用率;这是执行批处理,不是 token 间的信息交互。 4. **两次 all-to-all 是 EP 条件路径**:Expert 跨 rank 放置时,第一次 dispatch 把 hidden state 发到 expert owner,第二次 combine 把 top-k 份结果送回原 token/rank。单卡、本地 Expert 或其他通信实现不应机械地描述成两次跨卡 all-to-all。 不同模型还可能包含 shared expert、不同的 Norm/Residual 顺序或融合实现;这些不会改变“Attention 负责上下文,Expert FFN 负责逐 token 变换”的职责划分。路由、EP/TP 和通信边界见 [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/)。 ## 5. LM Head 与 Sampling 做什么 最后一层 hidden state 经 LM Head 投影到词表: ```text final_hidden_state: [H] LM Head weight: [V, H] logits: [V] ``` `V` 个 logits 是 `V` 个候选 token 的分数,不是一次生成 `V` 个 token。Sampling 再执行 temperature、top-k、top-p 或 greedy 等策略,选出 next token: ```text hidden state -> vocab logits -> sampling policy -> next token id ``` 有些模型会让 LM Head 与输入 embedding 共享权重;这改变参数存储合同,不改变“hidden state 投影为 logits”的职责。 ## 6. Prefill 与 Decode 如何复用这条线 两阶段运行的是同一套模型块,但 token 形状和 KV 行为不同: | 阶段 | 本次处理 | KV 行为 | 主要输出 | |---|---|---|---| | Prefill | prompt 的多个 token | 批量建立 KV Cache | 首个生成位置的状态 | | Decode | 每条活跃序列的一个新 token | 读取历史并追加当前 KV | 一个 next token | Serving pipeline 还包含 queue、scheduler、batch、cache allocation 和 response streaming;它是这条模型数据流的外层。 TIP: 不要把一次 forward 图直接当成完整请求时序。 ## 7. 看模型或模拟器图时怎么对齐 遇到配置数字时先问它属于哪一维: - `vocab_size`,例如 `129,280`:LM Head 输出维度。 - `top_k / num_experts`,例如 `6 / 384`:MoE 路由维度。 - `hidden_size`:层间主数据流的向量宽度。 - `num_kv_heads` 或 latent 维度:KV Cache 的每 token payload。 这些数字可能同时出现在一张图里,但不能互相换算。模拟器还必须区分模型块视角与 serving 视角,详见 [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/)。 ## 相关页面 - [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/) — MHA、KV 共享、表示压缩、稀疏访问与递推状态。 - [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) — Attention 历史状态的容量与生命周期。 - [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/) — Router、dispatch、expert compute 与 combine。 - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 把模型数据流映射到容量和吞吐合同。 --- ## LLM 推理系统全栈地图 > Source: https://weigao.cc/ai-systems/llm-inference/00-full-stack-synthesis/ > Date: 2026-07-26 > Tags: llm-inference, synthesis, systems, optimization 一套推理系统不是优化清单,而是一条受资源约束的数据流:请求进入调度器,模型执行 Prefill 和 Decode,状态进入 KV Cache,多卡之间交换激活或 Expert token,最终把 token 交付给用户。任何优化都应先指出它改变了哪段数据流、哪种资源压力和哪个服务指标。 :::important[30 秒复习] - **一句话**:先用算力、带宽、容量、通信定位主压力,再选择算法、Kernel、内存、调度、并行或 Serving 层的控制点。 - **三个判断**:Prefill 与 Decode 不能只靠阶段名称判瓶颈;容量会通过 Batch 间接改变吞吐;局部 Kernel 加速不等于 TTFT、TPOT 或容量同比改善。 - **核心模型**:`服务表现 = 数据流 × 资源上限 × 调度策略 × 负载分布`,缺少任意一项都无法解释端到端结果。 - **边界**:本文是导航和诊断地图,不维护框架功能矩阵,也不提供脱离 Case 的收益倍数。 ::: ![LLM 推理系统全栈架构图:从 API / Router 到 Serving Engine、Scheduler、KV Runtime、Model Algorithm、Kernel Runtime 和 Hardware / Fabric](https://weigao.cc/docs/ai-systems/llm-inference/images/full-stack-architecture-map.svg) ## 1. 先看数据流,再看优化名词 一次生成的主路径可以压缩为: ```text 请求 → tokenize / admission → Prefill:把 prompt 变成首轮 hidden state 与 KV → Decode:读取权重与历史 KV,逐步产生新 hidden state → LM Head / sampling → token 交付 ``` [Token Flow 与 Hidden State](https://weigao.cc/ai-systems/llm-inference/token-flow-hidden-state/)负责模型内部数据流;本页只增加系统侧的三个环: 1. **调度环**:哪些请求在本轮执行、各拿多少 token budget; 2. **状态环**:KV 在哪里分配、共享、迁移、回收; 3. **分布式环**:权重、激活、Expert token 或 KV 在哪些 rank 之间移动。 优化的第一问因此不是“要不要上量化”,而是: > 当前等待发生在数据流的哪一段,限制它的资源是什么? ## 2. 四种压力是共同坐标 ![优化打在哪个瓶颈上:把算力、带宽、容量和互联瓶颈分别映射到对应的优化杠杆](https://weigao.cc/docs/ai-systems/llm-inference/images/bottleneck-optimization-map.svg) | 压力 | 先看什么 | 常见控制点 | 主责页 | |---|---|---|---| | **计算** | Tensor Core 利用、算术强度、有效 FLOPs | 更合适的 Batch、低精度计算、算子形状 | [Roofline](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) | | **访存** | HBM 流量、权重/KV 字节、Kernel memory stall | 量化、KV 压缩、IO-aware Kernel、Batch 摊薄 | [量化](https://weigao.cc/ai-systems/llm-inference/03-quantization/)、[KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) | | **容量** | 权重、KV、workspace、碎片的作用域 | Paged KV、并行切分、量化、准入与抢占 | [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/)、[推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) | | **通信** | collective、点对点传输、拓扑和重叠 | TP/EP/PP/CP、放置、通信 Kernel、P/D 传输 | [推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) | ### Prefill / Decode 是提示,不是判决 Prefill 通常有更大的矩阵,Decode 通常反复读取权重和历史 KV,因此常被概括为“Prefill 偏计算、Decode 偏带宽”。这是一条有用的起点,却不是测量结果: - 短 prompt、小 Batch 的 Prefill 也可能利用率很低; - 大 Batch 的 Decode 会提高算术强度; - 长上下文 Decode 的 KV 读取可能超过权重读取; - MoE、跨节点并行和框架同步可能把瓶颈移到通信或运行时。 具体判断交给 [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/),不要用阶段名称代替 Roofline、Trace 和负载信息。 ## 3. 七层系统地图 ![LLM 推理系统层次划分:从服务层、引擎层、调度层、内存管理层、算法层、Kernel 层到硬件层](https://weigao.cc/docs/ai-systems/llm-inference/images/inference-layer-stack.svg) | 层 | 回答的问题 | 代表控制点 | 深读入口 | |---|---|---|---| | 服务与负载 | 谁在何时请求什么? | SLA、流量整形、路由、P/D 资源比 | [Serving Stack](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) | | 调度 | 本轮让谁执行多少? | Continuous Batching、Chunked Prefill、抢占 | [批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) | | 状态与内存 | KV 如何分配、共享和回收? | Paged KV、Prefix Cache、CoW、Swap | [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) | | 模型算法 | 从源头减少什么工作? | GQA/MLA、MoE、量化、投机解码 | [Attention 演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/)、[MoE](https://weigao.cc/ai-systems/llm-inference/moe-inference/) | | Kernel / Runtime | 如何少搬数据、少启动、少同步? | IO-aware attention、Fusion、Graph | [Kernel / Runtime](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) | | 并行 | 单卡放不下或算不过来时如何切? | DP/TP/PP/EP/CP | [推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) | | 硬件与互联 | 物理上限在哪里? | HBM、Tensor Core、NVLink、PCIe、IB | [GPU Architecture](https://weigao.cc/ai-systems/gpu-computing/gpu_arch/)、[GPU Communication](https://weigao.cc/ai-systems/gpu-computing/gpu_communication/) | “框架”不再单独承担所有知识。它的职责是把这些层组装成可部署的 Serving Stack;具体机制仍由各主责页解释。 ## 4. 优化为什么会相互影响 ### 4.1 容量 → Batch → 带宽摊薄 量化或 KV 压缩首先释放容量。容量变大后,调度器可能容纳更多并发;更大的有效 Batch 又能把一次权重读取摊给更多 token。于是观察到的吞吐提升可能来自两段: ```text 字节减少 → 单步读取更少 → 可用显存增加 → 有效 Batch 增大 → 每 token 权重成本继续下降 ``` 报告收益时应把“直接字节收益”和“容量带来的调度收益”分开,不能只用位宽比解释端到端倍数。 ### 4.2 Prefix Cache → Prefill 减少,但 Decode 不自动变快 Prefix 命中减少需要重新计算的 prompt 区间,主要影响 TTFT 和 Prefill 供给。它不会自动消除后续 Decode 的权重读取,也不保证 TPOT 同比例下降。Causal Attention 的命中面积由 [命中面积模型](https://weigao.cc/ai-systems/llm-inference/causal-attention-kv-hit-area/)解释,端到端修正由 [TTFT/TPM 修正模型](https://weigao.cc/ai-systems/llm-inference/kv-cache-hit-ratio-tpm-correction-model/)解释。 ### 4.3 Chunked Prefill → 更平滑,但可能牺牲单请求完成时间 把长 Prefill 切成多个调度块,可以给 Decode 插队,降低 ITL 抖动;同时也增加调度次数,并可能改变 Kernel 形状。它是在延迟分布、吞吐和公平性之间做预算,不是免费的加速开关。基础入口见[批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/),实现与实验见 [Chunked Prefill 深读](https://weigao.cc/ai-systems/llm-inference/chunked-prefill-deep-dive/)。 ### 4.4 并行 → 容量或计算改善,通信增加 增加 TP/EP 可以摊权重和计算,却会引入更频繁的 collective;PP 降低每 rank 层数,却带来流水线空泡。正确配置取决于模型、Batch、序列、拓扑和 SLA,不能从 GPU 数量直接推出性能。见[推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/)。 ### 4.5 Kernel 优化 → 局部时间下降,端到端受占比限制 若某 Kernel 占端到端时间的比例为 `p`,即使它无限加速,总体收益也受 `1 / (1-p)` 约束。更常见的情况是: - Kernel 更快后,调度或通信成为新瓶颈; - Fusion 减少 HBM 与 launch,却增加编译和支持成本; - CUDA Graph 降低 CPU launch 开销,却要求更稳定的形状与执行路径。 统一判断方法见 [Kernel / Runtime 优化](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/)。 ### 4.6 投机解码 → 少做大模型串行步,但要付验证成本 投机解码把多个候选 token 交给目标模型并行验证。收益由候选成本、接受长度、验证效率和负载共同决定;接受率不是吞吐倍数。稳定原理见[投机解码](https://weigao.cc/ai-systems/llm-inference/05-speculative-decoding/),具体 DSpark/MTP 见[实现调研](https://weigao.cc/ai-systems/llm-inference/dspark-vs-mtp/),Scheduler/KV 的提交边界见 [vLLM Async Scheduling 源码分析](https://weigao.cc/ai-systems/llm-inference/vllm-async-scheduling-speculative-decoding/)。 ## 5. 一套可复用的诊断顺序 ### 第一步:固定 Case 至少固定: - 模型与 checkpoint; - 硬件、rank 数和拓扑; - 输入/输出长度分布; - 并发或到达过程; - 精度、KV 格式和并行配置; - TTFT、TPOT/ITL、吞吐、容量与错误率目标。 没有 Case 身份的“更快”不可比较。 ### 第二步:分开服务结果与执行证据 服务层回答是否达标: - TTFT; - TPOT / ITL; - 请求吞吐与 token 吞吐; - P50 / P95 / P99; - OOM、抢占、拒绝和稳定性。 执行层解释为什么: - Queue / scheduler wait; - CPU launch / sync; - Kernel 与 HBM; - KV 使用与碎片; - collective 与网络; - 不同 rank / worker 的不平衡。 ### 第三步:找主压力 用 [Roofline](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/)、显存账本、Timeline 和通信证据判断主压力属于计算、访存、容量、通信还是运行时。多项同时存在时,应先处理约束有效容量或关键路径的控制点。 ### 第四步:选择最小干预 | 证据 | 优先尝试 | |---|---| | 权重/KV 字节主导 | 量化、KV 压缩、有效 Batch | | KV 容量或碎片限制 Batch | Paged KV、Prefix/CoW、准入、并行切分 | | 长 Prefill 干扰 Decode | Chunked Prefill、长短分流、P/D 分离 | | CPU launch / 小 Kernel 主导 | Fusion、Graph、编译或批量化 | | collective 在关键路径 | 并行度、放置、拓扑、重叠 | | Decode 串行步主导 | 投机解码,并测候选与验证开销 | ### 第五步:用同一 Case 回归 一次实验只改变一个主要控制点。除了目标指标,还要检查: - 尾延迟是否恶化; - 质量是否变化; - OOM、抢占或碎片是否上升; - 收益是物理执行改善,还是流量/缓存命中变化; - 瓶颈是否迁移。 ## 6. 证据边界 :::caution[不要把估算写成实测] - 理论下限用于判断量级,不是生产延迟承诺。 - Trace 百分比只能描述所采样 Case,不能直接解释整个集群或 P95。 - 模拟器给出的是指定假设下的容量/压力,不是校准后的生产能力。 - 框架支持随版本、硬件和 backend 变化,选型必须以目标版本和真实负载复测。 ::: 特别要分开三类量: 1. **理论或配置值**:根据模型结构、dtype、并行度计算; 2. **校准值**:用指定硬件和 workload 拟合; 3. **生产观测值**:带有时间窗、采样和请求分布。 三者可以互相校验,不能互相冒充。 ## 7. 怎么继续读 如果只想建立完整坐标,按本系列继续: 1. [Token Flow 与 Hidden State](https://weigao.cc/ai-systems/llm-inference/token-flow-hidden-state/) 2. [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/) 3. [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) 如果正在解决具体问题: - **显存 / 长上下文 / Prefix** → [KV Cache 专题](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) - **低精度 / 质量 / Kernel 是否真的执行** → [量化专题](https://weigao.cc/ai-systems/llm-inference/03-quantization/) - **排队 / ITL / Prefill 干扰** → [调度专题](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) - **MoE / 多卡 / collective** → [MoE](https://weigao.cc/ai-systems/llm-inference/moe-inference/) + [推理并行](https://weigao.cc/ai-systems/llm-inference/inference-parallelism/) - **小 Kernel / launch / HBM 往返** → [Kernel / Runtime](https://weigao.cc/ai-systems/llm-inference/kernel-runtime-optimization/) - **一次多出 token** → [投机解码](https://weigao.cc/ai-systems/llm-inference/05-speculative-decoding/) - **框架和 Serving 组合** → [推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) - **动手改变参数看压力迁移** → [推理性能优化实验室](/labs/inference-performance/) ## 相关页面 - [LLM 推理系统索引](https://weigao.cc/ai-systems/llm-inference/index/) — 所有专题、案例与来源的入口 - [Agentic Infra 推理优化](https://weigao.cc/ai-systems/llm-inference/agentic-infra-inference-optimization/) — 一份来源摘要中的 Profiling 闭环 - [Agentic AWP](https://weigao.cc/ai-systems/profiling/agentic-awp/) — 从观测走向 Breakdown 的来源摘要 - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 公式、作用域、配置与校准边界 --- ## KV Cache:推理性能的命根子 > Source: https://weigao.cc/ai-systems/llm-inference/01-kv-cache/ > Date: 2026-07-26 > Tags: LLM, Inference, KV Cache, PagedAttention, vLLM, Memory Management 自回归 decode 每步都会再次读取历史上下文。KV Cache 把各层已经计算出的 Key/Value 保存下来,使模型只需为新 token 生成新的 K/V;代价是常驻显存和每步历史读取量随序列增长。 :::important[30 秒复习] - **一句话**:KV Cache 用容量和带宽换掉历史 K/V 的重复投影计算,是自回归推理的基础状态。 - **三个判断**:先按模型 cache schema 算 bytes/token;PagedAttention 管碎片和映射而不改变语义 payload;prefix cache 命中、页共享与 offload/驱逐属于生命周期问题。 - **核心模型**:MHA/GQA 的逻辑容量为 `2 × layers × kv_heads × head_dim × tokens × bytes/element`,再乘请求数并按并行分片与 allocator 开销修正。 - **边界**:公式不是所有架构的统一答案;MLA、稀疏/滑窗、量化、TP 分片、Scale、页尾浪费和运行时 workspace 都会改变物理占用。 ::: ## 1. KV Cache 到底复用了什么 Attention 的完整坐标与 MHA/GQA/MLA 差异见 [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/)。这里只保留 KV 语义: ```text step t current hidden state -> Q_t, K_t, V_t Q_t reads cached K_1...K_t and V_1...V_t K_t, V_t append to cache attention output -> next model sublayer ``` 没有 cache 时,每个 decode step 都要重新从历史 hidden states 投影出 K/V;有 cache 后只投影新增 token,但当前 Query 仍需读取可见历史。它减少的是**重复生成历史 K/V**,不是取消历史 attention。 每层投影参数不同,因此每层都有自己的 cache。下图把 QKV 投影、decode 追加与跨请求 prefix 复用放在一起: ::diagram{slug="qkv-prefix-cache" title="QKV 与 Prefix KV Cache 复习图" aspect="16 / 9"} 还要区分两个容易混淆的复用: - **请求内 KV Cache**:同一序列 decode 时复用自己的历史状态。 - **跨请求 Prefix Cache**:另一个请求在匹配前缀上复用此前保存的状态。 二者使用相同类型的 payload,但命中判定、所有权和淘汰策略不同。 ## 2. 容量账:先算逻辑 payload 对 MHA/GQA/MQA,单个 token 的全模型逻辑 payload 为: $$ S_{\text{kv/token}} =2 \times L \times n_{\text{kv-heads}}\times d_{\text{head}}\times b_{\text{elem}} $$ 若 $B$ 条请求都缓存 $S$ 个 token: $$ M_{\text{logical}} =B\times S\times S_{\text{kv/token}} $$ | 变量 | 含义 | |---|---| | $2$ | K、V 两份 | | $L$ | Transformer 层数 | | $n_{\text{kv-heads}}$ | KV heads;不要误用 Query heads | | $d_{\text{head}}$ | 每个 KV head 的维度 | | $b_{\text{elem}}$ | cache payload 每元素字节数 | | $S$、$B$ | 每请求已缓存 token 与并发请求数 | 例如 $L=32$、$n_{\text{kv-heads}}=8$、$d_{\text{head}}=128$、BF16 时: $$ S_{\text{kv/token}} =2\times32\times8\times128\times2 =131{,}072\ \text{bytes}=128\ \text{KiB} $$ 一条 8K-token 请求的逻辑 payload 约为 1 GiB;32 条同长度请求约为 32 GiB。这里尚未计页尾浪费、Scale、对齐、元数据和 workspace。 ### 全局量、per-rank 量与物理量 模型级公式得到的是全局逻辑 payload。若 KV heads 沿 TP ranks 分片,理想均分下: $$ S_{\text{kv/token/rank}} \approx \frac{S_{\text{kv/token}}}{n_{\text{tp}}} $$ 但实际 layout 取决于 `num_kv_heads`、TP 映射、复制策略和 kernel。不能在未确认分片合同前机械除以 TP。 运行时真正占用通常是: ```text physical allocation = live payload + block rounding / fragmentation + quantization scales and alignment + allocator metadata + implementation-specific workspace ``` 因此模型可容纳的理论 tokens、allocator 可分配 tokens 和线上稳定容量是三个不同口径。 ## 3. Attention 架构如何改变 cache schema | 架构 | 每 token 每层主要缓存 | 变化点 | |---|---|---| | MHA | `2 × q_heads × head_dim` | 每个 Q head 有独立 K/V | | GQA | `2 × kv_heads × head_dim` | 多个 Q heads 共享一组 K/V | | MQA | `2 × head_dim` | 所有 Q heads 共享一组 K/V | | MLA | `latent KV + RoPE branch` | 缓存低维表示,不按 KV head 直接计数 | | Sliding/Sparse | 由可保留/可访问区域决定 | token 数不再等于完整历史 | | Recurrent | 固定或分层状态 | 不保存同形态的逐 token KV | 这张表只用于选公式。GQA 的训练与表达折中属于 Attention 主责页;MLA 的低秩、RoPE 解耦和矩阵吸收见 [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/)。 ## 4. PagedAttention 管的是什么 连续预留 `max_seq_len` 会产生页内浪费,也要求请求增长时寻找更大的连续空间。PagedAttention 借用虚拟内存思路,把逻辑 token blocks 映射到不连续的物理 cache blocks: ```text request block table logical block 0 -> physical block 7 logical block 1 -> physical block 2 logical block 2 -> physical block 15 ``` 运行时按需分配新 block,attention kernel 通过 block table 找到历史 K/V。它带来三个系统能力: 1. 请求无需预留一整段连续 `max_seq_len` 空间。 2. 活跃序列可独立增长、结束和回收物理 blocks。 3. 多个逻辑序列可以引用同一物理 prefix blocks。 PagedAttention **不会自动减少同一批 live tokens 的语义 payload**。它主要减少预留与碎片损失,并让共享、抢占和迁移更易实现;具体 block size 与 allocator 策略仍会影响页尾浪费和 kernel locality。 ## 5. Prefix Cache 与 Copy-on-Write 跨请求 prefix 命中必须满足运行时定义的等价条件,通常至少包括 token 序列、模型/权重或 adapter、位置语义,以及会影响 hidden state 的其他输入。字符串相同不一定代表 token 与执行上下文相同。 命中后,多个请求可以让 block table 指向同一组只读物理 blocks: ```text request A: [shared 0, shared 1, A-private 2] request B: [shared 0, shared 1, B-private 2] ``` 当请求要修改共享尾块时,Copy-on-Write 分配私有副本;完整只读 blocks 继续共享。由此必须分别观测: - prefix hit 的 token 比例; - 实际复用的物理 bytes; - CoW 复制与未满尾块的浪费; - 命中查找和加载引入的延迟。 “命中率高”并不自动等于“TTFT 必然按同比例下降”,端到端修正见 [KV Cache Hit Ratio 修正模型](https://weigao.cc/ai-systems/llm-inference/kv-cache-hit-ratio-tpm-correction-model/)。 ## 6. 一条 cache 的生命周期 从请求进入到资源回收,可以按以下状态审计: 1. **Lookup**:查询可复用 prefix;miss 不是零字节,而是需要新建的状态。 2. **Allocate**:为 miss tokens 预留逻辑 blocks 与物理 blocks。 3. **Populate**:prefill 计算并写入各层 K/V。 4. **Append**:decode 每步追加当前 token 的 K/V。 5. **Share / CoW**:只读 prefix 被其他请求引用,写入时拆分尾块。 6. **Preempt / Offload**:容量压力下暂停请求,或把部分 pages 迁到 Host/远端层。 7. **Evict / Free**:策略淘汰可复用 prefix,完成请求释放私有 blocks。 排障时应把“没有命中”“命中了但未驻留 GPU”“驻留但正在迁移”“被淘汰”分成不同状态,不能都折算为 cache miss 或零成本。 ## 7. 长上下文的四类控制手段 | 手段 | 控制对象 | 收益 | 边界 | |---|---|---|---| | GQA/MLA | bytes/token | 降低常驻容量与读取量 | 需要模型原生结构与匹配 kernel | | KV 量化 | bytes/element | 进一步缩小 payload | 要计 Scale/对齐并验证质量与实际 kernel | | Sliding/Sparse/Compression | 保留或访问的 token | 限制长上下文增长 | 改变可见区域或近似语义 | | Offload/Remote Cache | 驻留层级 | 用 Host/网络容量换 HBM | 增加传输和调度延迟 | 这些手段作用点不同,可以组合,但不能把理论位宽比、逻辑容量和校准后线上容量混成一个数字。 ## 8. 容量与性能检查表 1. 模型保存的是 MHA/GQA KV、MLA latent,还是其他状态? 2. 公式是全局量还是 per-rank,TP 下是否复制或均分? 3. dtype 是否包含 Scale、对齐与 metadata? 4. 统计的是 live payload、已分配 blocks,还是最大可分配容量? 5. prefix hit 是否需要从 Host/远端加载? 6. OOM 来自 KV、本轮 activation、graph pool,还是其他 workspace? 7. 结论是配置估算、allocator 实测,还是业务负载校准? 只有先统一这些口径,KV 容量、并发和 TTFT 才能比较。 ## 相关页面 - [Attention 架构演化](https://weigao.cc/ai-systems/llm-inference/attention-evolution/) — MHA/GQA/MQA、MLA、稀疏与递推结构的统一坐标。 - [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/) — latent KV 的 cache schema 与 serving 边界。 - [Causal Attention 命中面积](https://weigao.cc/ai-systems/llm-inference/causal-attention-kv-hit-area/) — prefix hit 后为何出现 `1-h²`。 - [KV Cache Hit Ratio 修正模型](https://weigao.cc/ai-systems/llm-inference/kv-cache-hit-ratio-tpm-correction-model/) — compute、load、overlap 与 TPM 口径。 - [批处理与调度](https://weigao.cc/ai-systems/llm-inference/04-batching-scheduling/) — cache block 与请求调度如何协同。 - [推理框架对比 2026](https://weigao.cc/ai-systems/llm-inference/inference-frameworks-2026/) — 当前 serving stack 的实现与选型边界。 --- ## 量化:从 Scale 到 W4A8 的完整坐标 > Source: https://weigao.cc/ai-systems/llm-inference/03-quantization/ > Date: 2026-07-26 > Tags: llm-inference, quantization, prefill, decode, gpu 量化是模型压缩与低精度执行技术,但它不是 zip 式无损压缩。它用更小的数值集合近似原始权重、激活或 KV Cache,并用 Scale 等元数据恢复数值范围。 只说“这是一个 FP4 模型”还不够。一个可复现的量化方案至少要写清量化对象、数值格式、Scale、配方、运行时和验收六个坐标: :::important[30 秒复习] - **一句话**:量化不是一个 dtype,而是“对象 × 值格式 × Scale 粒度 × 配方 × 运行时 Kernel × 验收”的近似执行合同。 - **三个判断**:先分清量化的是权重、激活还是 KV;再核对 Scale、累加与未量化层;最后用运行证据确认没有 fallback。 - **核心模型**:`q = clip(round(x / s))` 描述数值近似,容量账本和配置合同描述真实收益;位宽下降不等于实例显存或端到端性能同比下降。 - **边界**:方法名、Checkpoint 标签和理论字节数都不能替代同一 Case 下的质量、容量与性能验收。 ::: ![量化对象、配方、格式、Scale、运行时与验收组成的完整坐标](https://weigao.cc/docs/ai-systems/llm-inference/images/quantization-coordinate-map.svg) > **CPU 工程师入口**:可以把量化先理解为“Scale/舍入 → packing/layout → VNNI/AMX 或 Tensor Core → 宽精度累加 → profiler 验证”。完整对应关系见[从 AVX/AMX 到 Tensor Core](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/)。 ## 1. 量化在近似什么 ### 1.1 权重、激活和状态 | 对象 | 生命周期 | 常见写法 | 主要收益 | 主要风险 | |---|---|---|---|---| | 权重 `W` | Checkpoint 加载后长期驻留 | W4A16、W8A16 | 降低模型容量和权重读取流量 | 权重重构误差、Kernel fallback | | 激活 `A` | 每次 Forward 动态产生 | W8A8、W4A8 | 降低矩阵乘输入流量并使用低精度计算单元 | Outlier、动态 Scale 成本 | | KV Cache `State` | 随序列长度持续增长 | KV8、FP8 KV | 降低长上下文容量和读取流量 | Scale 校准、敏感层和长上下文误差 | | 累加与输出 | Kernel 内部或层间接口 | Acc=FP32、Out=BF16 | 稳定归约与层间连接 | 不能由 W/A 位宽自动推断 | 这三个对象可以独立选择精度。同一个模型里完全可能同时出现: ```text Linear: W=FP4, A=FP8, Acc=FP32, Out=BF16 Attention: Q/K/V=BF16, internal reduction higher precision KV Cache: State=FP8, read and convert according to kernel contract Norm/Router: BF16 or FP32-sensitive path ``` ### 1.2 最基本的映射 以对称整数映射为例: $$ q = \operatorname{clip}\left(\operatorname{round}\left(\frac{x}{s}\right), q_{\min}, q_{\max}\right), \qquad \hat{x} = s \cdot q $$ - $x$ 是原始高精度数值。 - $q$ 是量化后的有限离散值。 - $s$ 是 Scale,负责把有限编码映射到原始数值范围。 - $\hat{x}$ 是反量化后的近似值。 非对称整数方案还会加入 zero-point。浮点量化的编码方式不同,但同样需要回答“低精度值如何覆盖当前数据范围”。 ## 2. 位宽不是完整格式 | 格式 | 值本身 | 常见用途 | 必须额外确认 | |---|---|---|---| | BF16 | 1 sign + 8 exponent + 7 mantissa | 层间激活、Attention、输出 | Kernel 是否在内部提升累加精度 | | FP8 E4M3 | 1 sign + 4 exponent + 3 mantissa | 权重或激活、部分 KV Cache | Current/Delayed/Block scaling、Scale dtype | | FP8 E5M2 | 1 sign + 5 exponent + 2 mantissa | 更大动态范围的训练张量 | 推理引擎和硬件是否支持目标路径 | | INT8 | 有符号整数 | W8A8、部分 CPU/GPU 路径 | 对称/非对称、per-tensor/per-channel | | INT4 | 4 bit 整数 | GPTQ/AWQ 等 weight-only | group size、zero-point、packing、Kernel | | FP4 E2M1 | 1 sign + 2 exponent + 1 mantissa | Blackwell NVFP4 等路径 | block/global Scale、动态量化与设备能力 | `INT4`、`FP4` 只定义值域;`GPTQ`、`AWQ`、`NVFP4` 定义的是不同层次的算法或量化方案;`Marlin` 等名称则更接近运行时 Kernel。不要把这些层级放进同一列直接比较。 ## 3. Scale 粒度与 Outlier ![Per-tensor、per-channel、per-token 与 per-block Scale 的误差和运行时权衡](https://weigao.cc/docs/ai-systems/llm-inference/images/quantization-scale-granularity.svg) Scale 粒度回答的是:**多少个值共用同一把尺子?** | 粒度 | Scale 数量 | 直觉 | 常见位置 | |---|---:|---|---| | Per-tensor | 最少 | 一个 outlier 可能拉大整个张量的量化区间 | 简单 INT8/FP8 配方 | | Per-channel | 按通道 | 每个输出通道独立适配范围 | 权重量化 | | Per-token | 按 token | 动态适配当前激活 | Activation quantization | | Per-group / block | 按固定小块 | 更贴近局部分布,但增加元数据和 layout 约束 | INT4、MXFP8、NVFP4 | 更细的粒度通常能降低局部量化误差,但不是越细越好。真实成本还包括: $$ \text{bytes}_{\text{quantized}} \approx \frac{P \cdot b}{8} + \text{scale metadata} + \text{zero-point} + \text{padding/alignment} $$ 其中 $P$ 是参数量,$b$ 是低精度值的位宽。`70B × 4 bit = 35 GB` 只是裸权重下限,不是可运行实例的显存需求;运行时还需要 Scale、未量化层、KV Cache、activation、workspace 和通信缓冲。 Outlier 是激活量化尤其困难的原因。不同方法选择不同控制点: - AWQ 根据激活统计识别重要权重通道,通过等价缩放降低 weight-only 误差。 - SmoothQuant 把激活量化难度离线迁移到权重,使 W8A8 更容易执行。 - Rotation 类方法通过等价旋转重新分布 outlier。 - 工程上还可以保留敏感层或特定 Attention 层为高精度。 ## 4. 为什么量化可能加速,也可能不加速 ### 4.1 三条独立收益路径 1. **容量**:权重或 KV 变小,模型能放入更少 GPU,或容纳更大 Batch/Context。 2. **带宽**:每步从 HBM 读取的权重和 KV 字节减少。 3. **计算**:硬件和 Kernel 支持时,低精度矩阵乘拥有更高峰值吞吐。 这三条路径不能合并成一个固定倍数。 ### 4.2 Prefill 与 Decode 的差异 | 阶段 | 常见主瓶颈 | 更可能受益的方案 | 为什么不能预设倍数 | |---|---|---|---| | Prefill | 大矩阵计算、Attention IO | W8A8、FP8、W4A8 等低精度计算路径 | 取决于 Tensor Core、shape、fusion 和 Q/DQ 成本 | | 小 Batch Decode | 权重与 KV 的 HBM 读取 | W4A16、W4A8、KV8 | Kernel 解包、Scale、访存效率和未量化算子会吃掉理论收益 | | 大 Batch Decode | 逐步向 compute-bound 移动 | 低精度计算 + 调度 | Batch 改变 Roofline 位置,不能套用单请求结论 | | 长上下文 | KV 容量与 Attention 读取 | KV8、GQA/MLA 等 | 质量对 Scale、层类型和上下文长度敏感 | 因此应把理论上限写成假设: ```text 裸权重字节减少 4× ≠ 实例显存减少 4× ≠ TPOT 改善 4× ≠ 端到端吞吐改善 4× ``` 只有在同一硬件、引擎版本、模型、并行配置、ISL/OSL、Batch 和 SLO 下实测,才能给出性能倍数。 ## 5. 量化发生在什么时候 | 时机 | 做什么 | 优点 | 代价 | |---|---|---|---| | PTQ | 训练完成后,用权重和少量校准数据生成量化参数 | 成本低、适合已有模型 | 极低位或激活量化可能出现质量损失 | | QAT | 训练或微调时模拟量化误差 | 模型可以适应低精度噪声 | 需要训练数据、算力和稳定训练配方 | | Dynamic / online | 运行时根据当前激活计算 Scale | 能适应 token/block 的局部分布 | 引入 amax、Scale 计算、cast 和额外 Kernel | 这些时机可以组合,也不能推出方法的稳定排序。GPTQ、AWQ、SmoothQuant、Rotation 和低精度 recipe 控制的是不同误差点;具体方法与实验设计统一放在[量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/)。 ## 6. 部署时真正要核对什么 ### 配置合同 ```text Model identity: checkpoint + revision + model config Quantization recipe: object + format + granularity + calibration + excluded layers Runtime identity: engine version + kernel backend + hardware + TP/PP/EP Workload identity: ISL/OSL + batch/concurrency + KV hit + SLO ``` ### 验收顺序 1. 配置回读确认加载了预期 recipe。 2. 引擎日志和 trace 确认 operand dtype、Q/DQ 边界与 Kernel,没有静默 fallback。 3. 与高精度基线做质量验收。 4. 分别核对权重、Scale、KV、activation 和 workspace 的容量账本。 5. 再比较 TTFT、TPOT、TPS/TPM 与成本。 ## 7. 关键认知 - 量化是一组近似和执行合同,不是单一 dtype。 - `W4A8` 只描述某次矩阵乘的两个 operand 位宽。 - Scale 粒度、Scale dtype 与 outlier 处理共同决定误差。 - Checkpoint 更小不等于运行时一定使用低精度 Kernel。 - 理论字节下降不等于实测延迟或吞吐同比例改善。 - 质量、容量和性能是三个独立结果,必须分别验收。 ## 相关页面 - [CPU 工程师理解量化](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/) — 从 INT8、AMX、Cache 和 perf/IBS 迁移到 Tensor Core、HBM 与 GPU trace - [量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/) — PTQ/QAT 方法族、质量门禁和可复现实验矩阵 - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) — FP8 scaling、NVFP4 与运行时能力边界 - [GLM-5.2 量化执行图](https://weigao.cc/ai-systems/llm-inference/glm52-operator-quantization/) — 一个模型内部的 W4A8、BMM、Attention 与 KV8 - [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) — 用 Roofline 判断量化打在哪个瓶颈上 - [KV Cache](https://weigao.cc/ai-systems/llm-inference/01-kv-cache/) — KV 容量、数据布局与长上下文影响 ## 参考资料 - [GPTQ](https://arxiv.org/abs/2210.17323) — 基于近似二阶信息的 weight-only PTQ。 - [AWQ](https://arxiv.org/abs/2306.00978) — 激活感知的重要权重通道保护。 - [SmoothQuant](https://arxiv.org/abs/2211.10438) — 面向 W8A8 的 activation smoothing。 - [vLLM Quantization](https://docs.vllm.ai/en/latest/features/quantization/) — 当前量化方法和硬件兼容矩阵;兼容性会随版本变化。 - [TensorRT Quantization Schemes](https://docs.nvidia.com/deeplearning/tensorrt/latest/inference-library/quantized-types-schemes.html) — INT8、FP8、MXFP8、INT4 与 NVFP4 的格式和粒度合同。 --- ## 从 AVX/AMX 到 Tensor Core:CPU 工程师理解 LLM 量化 > Source: https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/ > Date: 2026-07-26 > Tags: llm-inference, quantization, cpu, gpu 如果你做过 CPU 性能优化,可以先把 LLM 量化理解为:**用 Scale 把数值变成更窄的 operand,按硬件要求打包成 tile,送入低精度矩阵乘单元,用更宽的类型累加,最后用 profiler 证明实际走了这条路径。** CPU 与 GPU 的数学没有换一套。真正变化的是并行规模、存储层次、支持的低精度格式,以及运行时如何选择 Kernel。 :::important[30 秒复习] - **一句话**:CPU 与 GPU 量化共享“窄 operand → 专用矩阵单元 → 宽精度累加 → 运行证据”的骨架,差别在执行体系和存储层次。 - **三个判断**:先拆开值格式与 Scale;再核对 layout、shape 和累加类型;最后用日志与 trace 证明专用路径生效。 - **核心模型**:`value format + Scale + granularity` 定义 operand,`dtype + layout + shape + hardware` 决定最终执行路径。 - **边界**:Block Scale 不是 Cache Line,Tensor Core 也不是 GPU 版 AMX;类比只用于迁移问题结构,不能替代设备语义。 ::: ![CPU 的 INT8/VNNI/AMX 心智模型如何映射到 GPU 的 FP8/FP4、Tensor Core 与运行时验证](https://weigao.cc/docs/ai-systems/llm-inference/images/cpu-gpu-quantization-bridge.svg) ## 1. 先迁移这六个 CPU 心智模型 | CPU 里熟悉的概念 | 量化/GPU 中对应的问题 | 可以迁移的直觉 | 不能直接画等号 | |---|---|---|---| | INT8、定点数、饱和与舍入 | Quantize、Scale、zero-point | 有限编码必须覆盖原始数值范围 | FP8/FP4 是非均匀浮点编码,不是小号 INT | | AVX-512 VNNI、AMX Tile | Tensor Core MMA/GEMM | 专用指令只有在 operand、layout、shape 合同时才生效 | AMX 属于 CPU core,Tensor Core 属于 GPU SM 的并行执行体系 | | INT8 点积后 INT32 累加 | W4A8、Acc=FP32/INT32 | 输入精度和累加精度必须分开描述 | `W4A8` 本身不说明 accumulator | | Cache Line、alignment、packing | group/block Scale、tensor layout | 数据布局会决定 load 和计算效率 | Block 是数值共享 Scale 的范围,不是硬件 Cache Line | | Memory Wall、Roofline | Decode 权重/HBM 带宽瓶颈 | 先算 bytes,再判断瓶颈是否在带宽 | GPU 的 HBM/L2/Shared Memory 不是 CPU DRAM/LLC/L1 的简单改名 | | perf、PMU、IBS | GPU trace、Kernel dtype、HBM counter | 声明的配置必须由运行证据确认 | 两边的事件语义、采样机制和并行归因不同 | 下面按这六个锚点展开。 ## 2. Scale 就是你熟悉的“表示范围合同” CPU 上做定点数或 INT8 推理时,核心动作是把连续数值映射到有限整数集合。对称量化可写成: $$ q = \operatorname{clip}\left(\operatorname{round}\left(\frac{x}{s}\right), q_{\min}, q_{\max}\right), \qquad \hat{x} = s \cdot q $$ 从 CPU 视角看: - `q` 类似实际进入向量/矩阵指令的窄类型 operand; - `s` 是窄类型编码与原始数值范围之间的合同; - `clip` 对应超出表示范围后的饱和; - rounding 与 saturation 共同产生量化误差; - zero-point 只属于部分非对称整数方案,不是所有量化格式都有。 GPU 上的 FP8/FP4 仍然需要回答同一个问题:如何让有限编码覆盖张量分布。区别是 FP 格式的值分布不均匀,并且可能使用 current、delayed 或 block scaling。 因此最稳定的理解不是“FP8 比 INT8 高级”,而是: ```text value format 决定有哪些离散值 + Scale 决定这些值覆盖哪段真实范围 + granularity 决定多少元素共享一把尺子 ``` ## 3. VNNI、AMX 和 Tensor Core 都在做什么 ### 3.1 从标量、向量到 Tile | 执行方式 | 数据组织 | 典型工作 | |---|---|---| | 标量 ALU | 单个或少量寄存器值 | 普通算术、控制与尾部处理 | | AVX-512 VNNI | 一维 SIMD 向量 | 把多步 INT8 multiply-add 合并为点积累加 | | Intel AMX | 二维 Tile register | 让 TMUL 对 tile 执行矩阵乘累加 | | NVIDIA Tensor Core | GPU 矩阵 fragment/tile | 在大量并行线程协作下执行低精度 MMA | Intel 官方资料给出的经典 AMX 合同是: ```text INT8 × INT8 → INT32 accumulate BF16 × BF16 → FP32 accumulate ``` 这和 GPU 的关键共性是:**窄类型主要用于输入与乘法吞吐,累加通常使用更宽的类型保护数值稳定性。** GPU 上则必须按具体架构和 Kernel 核对: ```text (operand A dtype, operand B dtype, accumulator dtype, output dtype, tile shape, scale layout) → executed Tensor Core path ``` 所以 `W4A8` 类似 CPU 代码里只告诉你两个输入的类型,却没有告诉你使用哪条 instruction、如何 packing、累加到哪里,以及尾部是否 fallback。 ### 3.2 Checkpoint 与 ISA binary 是同一类边界 CPU 工程师不会因为源码里出现 `int8_t` 就断言机器一定执行了 VNNI/AMX;还会检查: - 编译器是否向量化; - CPU feature 是否开启; - oneDNN/OpenVINO 选择了什么 primitive; - layout、shape 和 tail 是否让专用路径生效; - 热点里是否仍是 unpack、reorder 或普通指令。 GPU 量化完全一样: - Checkpoint 写着 FP4,不代表执行了 FP4 Tensor Core; - Engine 可能重打包、cast、dequant、pre-expand、fallback 或拒绝; - 只有加载日志、显存回读和 Kernel trace 能证明最终路径。 ## 4. Cache Line 与 Block Scale:可以类比,但不是一回事 二者的共同点是都在定义“一个管理单元”: - Cache Line 定义内存层次搬运和一致性的基本数据块; - quant block/group 定义多少个低精度值共享一个 Scale。 但它们解决的是不同问题: | 维度 | Cache Line | Quant block/group | |---|---|---| | 控制对象 | 存储层次的数据搬运与命中 | 数值范围与量化误差 | | 典型单位 | byte-addressed hardware block | tensor 某一维的连续元素组 | | 元数据 | tag、valid、coherence state | Scale、可选 zero-point | | 主要代价 | miss、带宽浪费、false sharing | Scale 开销、padding、Q/DQ 与 Kernel 限制 | 它们会在 Kernel 中相遇:如果 block layout 让 Scale 和 packed value 访问不连续,最终仍会表现为低 load efficiency、额外事务或更多转换。 ## 5. Memory Wall:CPU 的公式可以直接迁移 CPU 上你会用工作集大小、Cache miss 和 DRAM 带宽估算下限。GPU Decode 也可以先写: $$ T_{\mathrm{weight}} \gtrsim \frac{ \text{weight payload} + \text{Scale metadata} + \text{padding} }{ \text{effective HBM bandwidth} } $$ 从 BF16 降到 4 bit,只能先推出裸权重 payload 下降;不能直接推出 TPOT 改善 4 倍,因为端到端还包含: $$ T_{\mathrm{e2e}} = T_{\mathrm{weight}} + T_{\mathrm{KV}} + T_{\mathrm{Q/DQ}} + T_{\mathrm{compute}} + T_{\mathrm{communication}} + T_{\mathrm{schedule}} $$ 这和 CPU 上“结构体缩小 4 倍,不代表程序快 4 倍”是同一个道理:瓶颈可能转移,新增 unpack、分支、尾部处理和访存不连续也会吃掉收益。 ## 6. 从 perf/IBS 迁移到 GPU Trace 不要背 GPU 工具名字,先沿用你熟悉的证据问题: | 要证明什么 | CPU 常用证据 | GPU 对应证据 | |---|---|---| | 是否执行目标低精度指令 | disassembly、PMU、library verbose | Kernel 名、operand dtype、engine log | | 是否受内存墙限制 | LLC miss、DRAM BW、stall、IBS load latency | HBM throughput、L2、memory/SOL、Kernel timeline | | layout 是否低效 | split load、unaligned、Cache miss | transaction efficiency、cast/reorder、短 Kernel | | 是否发生 fallback | scalar tail、generic primitive | 高精度替代 Kernel、Q/DQ、pre-expand | | 端到端是否受益 | cycles/request、QPS、P95 | TTFT、TPOT、TPS/TPM、成本 | 证据顺序也一样: 1. 固定 workload 和软件版本。 2. 确认实际执行路径。 3. 判断 compute、memory 还是调度瓶颈。 4. 对比相同 Case 的端到端指标。 5. 质量、容量和性能分别下结论。 ## 7. CPU 工程师的最短学习路径 建议按以下顺序读,不需要先补完整 CUDA: 1. **Scale 与量化误差**:先稳住数值表示。 2. **W/A/Acc/Out**:像读 ISA operand 一样读精度合同。 3. **AMX → Tensor Core**:理解 tile 矩阵乘和宽精度累加。 4. **Memory Wall → HBM**:用 Roofline 判断量化打在哪个瓶颈。 5. **perf/IBS → GPU trace**:用运行证据证明 Kernel,而不是相信标签。 6. 最后再扩展到 KV Cache、MoE、TP/EP 和服务调度。 核心主线始终只有一句: > **先确认数据如何表示,再确认硬件如何执行,最后确认瓶颈和端到端结果是否真的改变。** ## 相关页面 - [量化基础](https://weigao.cc/ai-systems/llm-inference/03-quantization/) — Scale、W/A/Acc/Out 与性能边界 - [量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/) — PTQ/QAT 和四道验收门 - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) — FP8/FP4 Scale recipe 与 Tensor Core 合同 - [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) — GPU Roofline 与 Prefill/Decode 瓶颈 - [Intel AMX 指令](https://weigao.cc/cpu-gpu/cpu-architecture/instruction-sets/amx/) — Tile register 与 TMUL - [AMD IBS](https://weigao.cc/cpu-gpu/cpu-architecture/pipeline-performance/ibs/) — L3 miss、访存延迟和 NUMA 归因 ## 参考资料 - [Intel AMX Overview](https://www.intel.com/content/www/us/en/products/docs/accelerator-engines/what-is-intel-amx.html) - [Intel AMX AI Tuning Guide](https://www.intel.com/content/www/us/en/developer/articles/technical/tuning-guide-for-ai-on-the-4th-generation.html) - [Intel AMX INT8 Code Sample](https://www.intel.com/content/www/us/en/developer/articles/code-sample/advanced-matrix-extensions-intrinsics-functions.html) - [NVIDIA TensorRT Quantization Schemes](https://docs.nvidia.com/deeplearning/tensorrt/latest/inference-library/quantized-types-schemes.html) - [NVIDIA FP8 Current Scaling](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/fp8_current_scaling/fp8_current_scaling.html) --- ## FP4/FP8 量化:值域、Scale 与运行时合同 > Source: https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/ > Date: 2026-07-26 > Tags: llm-inference, quantization, gpu 讨论 FP4/FP8 时必须拆成三层:E4M3、E5M2、E2M1 等**值格式**,Scale 的时间来源、共享粒度与自身类型,以及设备和引擎最终执行的**运行时路径**。少写其中一层,就无法判断模型到底存了什么、算了什么。 旧式“FP8 固定使用 per-128×128”“NVFP4 就是 per-32 E8M0”“MXFP4 等于 NVFP4”都不成立。数值格式和 Scale recipe 不能混为一谈。 :::important[30 秒复习] - **一句话**:FP4/FP8 只有与 Scale recipe 和实际 Kernel 一起描述,才是一份可执行的低精度合同。 - **三个判断**:E4M3/E2M1 不定义 Scale 粒度;NVFP4 不等于 MXFP4;Checkpoint 标签不能证明原生低精度执行。 - **核心模型**:用 `低精度值 × block Scale × global Scale` 还原数值,再用 `checkpoint + recipe + engine + architecture + shape` 定位实际路径。 - **边界**:理论 payload 不是实例显存,硬件峰值也不是端到端 TPS;版本、shape、padding 与 fallback 都要进入验收。 ::: ![FP8 Current、Delayed、MXFP8 与 NVFP4 的 Scale 合同](https://weigao.cc/docs/ai-systems/llm-inference/images/fp4-fp8-scaling-contracts.svg) > **从 AMX 迁移的直觉**:E4M3/E2M1 对应低精度 operand 格式,Scale 对应表示范围合同,Tensor Core 类似专用 tile 矩阵乘单元,FP32 accumulator 则对应 AMX 中“窄输入、宽累加”的数值保护。类比只到执行合同为止;两者的并行规模、存储层次和支持格式并不相同。 ## 1. FP8 值格式 NVIDIA Transformer Engine 当前定义两种 FP8: | 格式 | 位布局 | 最大有限幅值 | 取舍 | |---|---|---:|---| | E4M3 | 1 sign + 4 exponent + 3 mantissa | 448 | 尾数更多,精度相对更高 | | E5M2 | 1 sign + 5 exponent + 2 mantissa | 57344 | 动态范围更大,精度相对更低 | 这张表只描述**值本身**。同一个 E4M3 张量可以使用 per-tensor、per-channel 或 block scaling,Scale 也可能来自当前 amax、历史 amax 或每个 block 的动态计算。 ## 2. FP8 的三种典型 Scale 配方 ### 2.1 Current Scaling Current Scaling 根据当前张量的 amax 计算 Scale,然后再执行 cast: $$ s = \frac{\operatorname{amax}(x)}{\operatorname{max}_{\mathrm{FP8}}}, \qquad x_q = \operatorname{cast}_{\mathrm{FP8}}\left(\frac{x}{s}\right) $$ 它能跟随当前分布,但通常需要一次读取求 amax、再读取并转换。 ### 2.2 Delayed Scaling Delayed Scaling 使用历史 amax 估计当前 Scale。它减少了本次量化前的张量扫描,但 Scale 对突发分布变化的响应更慢。 Current 与 Delayed 都可以使用 E4M3/E5M2;区别在 Scale 的**时间来源**,不是值格式。 ### 2.3 MXFP8 MXFP8 是 microscaling FP8: - 数据值使用 FP8 E4M3。 - 每 32 个连续元素共享一个 E8M0 Scale。 - Scale 是 2 的幂,适合硬件 block scaling。 - 每个 block 独立,减少整个张量被少数 outlier 拉宽的问题。 MXFP8 的 `32 + E8M0` 不能套到 NVFP4 上。 ## 3. NVFP4 的分层 Scale NVFP4 的低精度值采用 E2M1,可表示的幅值集合为: ```text 0, ±0.5, ±1, ±1.5, ±2, ±3, ±4, ±6 ``` Transformer Engine 的 NVFP4 使用分层 scaling: $$ x \approx x_{\mathrm{E2M1}} \cdot s_{\mathrm{block}} \cdot s_{\mathrm{global}} $$ - `x_E2M1`:4 bit E2M1 值。 - `s_block`:每 16 个连续元素共享的 FP8 E4M3 Scale。 - `s_global`:整个张量共享的 FP32 Scale。 对于权重,Transformer Engine 默认还可以采用 16×16 的二维 scaling;激活和梯度使用一维 16-element block。二维权重 Scale 的布局不能用一维公式直接估算。 ### NVFP4 与 MX 家族不是同义词 | 方案 | 数据值 | Local Scale | Block | 额外全局 Scale | |---|---|---|---:|---| | MXFP8 | FP8 E4M3 | E8M0 | 32 | 无 | | NVFP4 | FP4 E2M1 | FP8 E4M3 | 16 | FP32 per-tensor | | OCP MXFP4 | FP4 E2M1 | E8M0 | 32 | 无 | NVFP4 与 MXFP4 都使用 E2M1,不代表它们拥有相同的 Scale 类型、block size、数值误差或 Kernel 合同。 ## 4. 从 Checkpoint 到 Kernel Checkpoint 中出现 `fp4` 或 `nvfp4`,只能证明存储或 recipe 元数据;不能自动证明硬件执行了原生 FP4 MMA。 真实路径由以下联合决定: ```text (checkpoint format, recipe and scale layout, engine version, kernel backend, GPU architecture, operand shape) → executed runtime path ``` 可能结果包括: | 结果 | 含义 | 验证证据 | |---|---|---| | Native | 低精度 operand 直接进入目标 Tensor Core 路径 | Kernel 名、operand dtype、设备能力 | | Cast / dequant | 存储为低精度,计算前转换为 FP8/BF16 | Q/DQ 或 cast Kernel、额外中间张量 | | Pre-expand | 加载阶段展开为更高精度常驻 | 加载日志、实际 HBM、权重 buffer dtype | | Fallback / reject | 不支持该 recipe 或 shape | Warning/error、替代 Kernel、性能与容量异常 | Hopper 支持原生 FP8 Tensor Core;原生 FP4 计算属于 Blackwell 及之后的设备能力。H100/H200 如何处理一个 FP4 Checkpoint 不是统一答案:引擎可能转换、展开、回退或拒绝,因此不能直接写死“显存一定翻倍”。 ## 5. Scale Metadata 的容量账本 以下只用于理解数量级,真实实现还要加 padding、alignment、transpose copy 和未量化层: | 配方 | 数据 bytes/value | Scale 开销 | 备注 | |---|---:|---:|---| | FP8 per-tensor | 1 | 每张量约一个 FP32 Scale | 不包含 amax history | | MXFP8 | 1 | 每 32 值一个 E8M0 byte | 约 `1 + 1/32` bytes/value | | NVFP4 1D | 0.5 | 每 16 值一个 E4M3 byte + 全局 FP32 | 约 `0.5 + 1/16` bytes/value | | NVFP4 2D weight | 0.5 | 按 16×16 tile 组织 Scale | 以实际布局和 padding 计算 | 模拟器不应只存一个 `dtype -> bytes` 映射。至少要记录: ```text value format scale format scale granularity packing and padding runtime expansion excluded high-precision tensors ``` ## 6. FP4/FP8 的性能边界 ### 容量收益 只要低精度数据保持压缩存储,权重或 KV 的 HBM 占用就会下降;但比例要包含 Scale、padding 和高精度例外。 ### 带宽收益 Decode 等 memory-bound 路径可能因为读取字节减少而受益。前提是解包、Scale 读取、cast 和不连续访问没有抵消收益。 ### 计算收益 只有设备和 Kernel 对目标 operand 组合提供原生支持,低精度峰值算力才有意义。不要用硬件峰值表直接推导端到端 TPS: $$ T_{\mathrm{end-to-end}} = T_{\mathrm{quant}} + T_{\mathrm{GEMM/BMM}} + T_{\mathrm{attention}} + T_{\mathrm{communication}} + T_{\mathrm{framework}} $$ 对于混合精度模型,每个算子应使用自己的精度峰值和实际时间建模,而不是把 FP4、FP8、BF16 FLOPs 相加后统一除以一个峰值。 ## 7. 实际核对清单 ### Checkpoint - 权重值格式是什么? - Scale dtype、shape 和 block size 是什么? - 哪些层没有量化? - 是否同时保存 rowwise/columnwise 或 transpose copy? ### Runtime - 引擎版本是否支持该 recipe? - 设备 compute capability 是否支持目标低精度 Kernel? - 日志和 trace 中的 operand dtype 是什么? - 是否出现 cast、dequant、pre-expand 或 fallback? ### Acceptance - 实际峰值 HBM 是否与容量账本一致? - TTFT、TPOT、TPS 是在完全相同 Case 下比较的吗? - 质量是否覆盖 PPL、任务分数、长上下文和业务样本? ## 相关页面 - [CPU 工程师理解量化](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/) — AMX/VNNI 与 Tensor Core 的共性和边界 - [量化基础](https://weigao.cc/ai-systems/llm-inference/03-quantization/) — 量化对象、Scale 粒度和性能边界 - [量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/) — PTQ/QAT 方法族和四道验收门 - [GLM-5.2 量化执行图](https://weigao.cc/ai-systems/llm-inference/glm52-operator-quantization/) — 混合精度如何落到真实算子 - [MoE 推理](https://weigao.cc/ai-systems/llm-inference/moe-inference/) — Expert 权重、Grouped GEMM 与 EP - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 精度和 metadata 如何进入容量与吞吐模型 ## 参考资料 - [FP8 Current Scaling](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/fp8_current_scaling/fp8_current_scaling.html) - [FP8 Delayed Scaling](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/fp8_delayed_scaling/fp8_delayed_scaling.html) - [MXFP8](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/mxfp8/mxfp8.html) - [NVFP4](https://docs.nvidia.com/deeplearning/transformer-engine/user-guide/features/low_precision_training/nvfp4/nvfp4.html) - [TensorRT Quantization Schemes](https://docs.nvidia.com/deeplearning/tensorrt/latest/inference-library/quantized-types-schemes.html) - [OCP Microscaling Formats Specification](https://www.opencompute.org/documents/ocp-microscaling-formats-mx-v1-0-spec-final-pdf) --- ## GLM-5.2 量化执行图:W4A8、BMM 与 KV8 > Source: https://weigao.cc/ai-systems/llm-inference/glm52-operator-quantization/ > Date: 2026-07-26 > Tags: llm-inference, quantization, fp4, fp8, nvfp4, profiling 同一个模型会按算子采用不同精度。本文整理一张面向 GLM-5.2 Prefill、L20X 与 vLLM 的**示例执行图**,用来说明如何核对精度合同;`applies_to` 表示建模对象,不表示当前页面已经记录了可审计的 Trace 身份。 当前材料没有附模型 revision、vLLM commit、Trace ID、采集配置和原始产物,因此下文的具体路径都应视为**待 Trace 复核的候选**,不能当作该组合的实测事实。描述单个算子的坐标系是: $$ \mathcal{P}_{\text{operator}} = \left(W, A, \mathrm{Acc}, \mathrm{Out}, \mathrm{State}\right) $$ 在这个范围内,`W4A8` 表示一次 GEMM 使用 FP4 权重和 FP8 激活;它不代表整张图都是 4/8 bit。Attention 核心的 `QKᵀ`、`PV` BMM 没有常驻模型权重 operand `W`,但完整 Attention 模块的 Q/K/V/O 投影仍有权重;KV Cache 则属于状态 `State`,必须分别描述。下图先给出候选精度路径;需要逐节点查看 `W / A / Acc / Out / State` 时,可[打开完整交互图](/diagrams/glm52-quantization-flow/index.html)。 :::important[30 秒复习] - **一句话**:这张示例图说明 Prefill 应按算子核对混合精度,而不是用一个 `W4A8` 标签覆盖整张计算图。 - **三个判断**:Linear 看 `W/A/Acc/Out`;Attention 与 BMM 按各自 operand 看;KV Cache 要作为 `State` 单独核对读取和转换。 - **来源问题**:配置声明了什么、Trace 实际执行了什么 Kernel、转换边界在哪里,三者必须能对应到同一模型与运行身份。 - **边界**:模型 revision、vLLM commit、Trace ID、采集配置与原始产物缺失时,具体 dtype 路径只能作为待核验候选,也不能外推为其他模型或 Decode 路径。 ::: ![GLM-5.2 同一层中的 W4A8、低精度 BMM、BF16 Attention 与 KV8 候选精度路径](https://weigao.cc/docs/ai-systems/llm-inference/images/glm52-precision-contract.svg) ## 先读懂图里的格式 图中用 FP4 E2M1 表示权重或低精度 BMM operand,用 FP8 E4M3 表示动态量化后的激活或 KV 状态,用 BF16 表示层间张量和 Attention。值格式、Scale 粒度与 runtime recipe 的完整解释由 [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/)维护;这里仅用它们标注待核验的算子边界。 ## GEMM 与 BMM GEMM 完成一组矩阵乘法: $$ A_{M \times K} W_{K \times N} \longrightarrow Y_{M \times N} $$ 一条候选执行路径可以写成: ```text BF16 hidden → 动态量化为 FP8 E4M3 → FP8 activation × FP4 E2M1 weight → FP32 accumulate → BF16 output ``` 只有 Trace 证实上述 operand、累加和输出 dtype 后,才能把这条路径记作 `W4A8`。 BMM 同时执行一批矩阵乘法: $$ X^{(b)}_{M \times K} Y^{(b)}_{K \times N} \longrightarrow Z^{(b)}_{M \times N}, \qquad b = 1, 2, \ldots, B $$ BMM 两侧经常都是运行时张量,因此直接写两个 operand 的精度更清晰。本页用 `FP4 × FP4` 和 `BF16 × FP4` 作为两条候选路径;实际采用哪条必须由同一运行身份下的 Trace 证明。 ## 候选算子级精度合同 | 模块 | 待核验的候选精度 | 主要目标 | |---|---|---| | Linear / 部分 MoE GEMM | W=FP4,A=FP8,Acc=FP32,Out=BF16 | 降低权重容量与带宽,使用低精度 Tensor Core | | 部分 DSA / MLA BMM | FP4×FP4 或 BF16×FP4 | 按 BMM 的 operand 和 shape 选择 kernel | | Sparse Attention | Q/K/V=BF16,Out=BF16 | 支撑点积、归约和 Softmax 的数值范围 | | KV Cache | State=FP8,读取后转换为 BF16 | 降低长上下文 Cache 容量和读取流量 | 若 Trace 中出现 `KVGatherUpconvert`,应核对它是否从 FP8 Cache 聚合 K/V 并输出 BF16 K/V。该过程本身没有模型权重 operand,属于状态读取和精度转换。 ## 如何核对这张执行图 1. 从模型结构列出 Linear、BMM、Attention 与 KV Cache 节点。 2. 用量化配置标注每个节点声明的 operand、Scale、累加、输出与状态精度。 3. 用 Trace 核对实际 Kernel、shape、cast 与 `KVGatherUpconvert` 等转换边界。 4. 只有前三步一致,才能把对应 dtype 和转换成本写进仿真;不一致处应标记为待核验或 fallback。 ## 相关页面 - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) — 低精度格式、scale metadata 与硬件能力 - [量化基础](https://weigao.cc/ai-systems/llm-inference/03-quantization/) — 对象、格式、Scale 粒度与运行时合同 - [量化方法与评测](https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/) — PTQ/QAT 方法谱系与四道验收门 - [DeepSeek MLA](https://weigao.cc/ai-systems/llm-inference/deepseek-mla/) — MLA、矩阵吸收与 KV Cache 数据路径 - [模拟器建模指南](https://weigao.cc/ai-systems/llm-inference/simulator-modeling-guide/) — 精度选择如何进入显存与吞吐公式 --- ## 量化方法与评测:从 PTQ/QAT 到可复现实验 > Source: https://weigao.cc/ai-systems/llm-inference/quantization-methods-evaluation/ > Date: 2026-07-26 > Tags: llm-inference, quantization, profiling, research 量化方法不该按名字背诵,而应按它控制的误差点来理解:权重舍入与补偿、重要通道保护、activation outlier 迁移或重排、训练中适应低精度噪声,以及 KV Cache 状态压缩。 选定方法只是开始。一个方案还必须分别通过执行真实性、模型质量、容量收益和性能收益四道门;任何一道门都不能替代另外三道。 :::important[30 秒复习] - **一句话**:方法名只说明误差控制思路,生产结论必须通过 G1 执行、G2 质量、G3 容量、G4 性能四道独立验收门。 - **三个判断**:方法没有脱离 Case 的总排序;先证明没有 fallback,再谈收益;`missing`、`unsupported`、`failed` 和 `not_comparable` 都不是零。 - **核心模型**:按 `G1 → G2 → G3 → G4` 顺序验证,并固定模型、recipe、引擎、硬件、并行、负载与 SLO 身份。 - **边界**:一个 PPL、一个速度点或理论位宽都不能外推到另一个模型、Kernel、Batch 或上下文长度。 ::: ![量化方案从运行时、质量、容量到性能的四道验收门](https://weigao.cc/docs/ai-systems/llm-inference/images/quantization-evaluation-gates.svg) ## 1. 先按控制点分类 ### 1.1 Weight-only PTQ Weight-only 方案保留较高精度激活,典型写法是 W4A16。 | 方法 | 核心控制点 | 需要校准数据 | 典型特点 | |---|---|---:|---| | RTN | 直接 round-to-nearest | 否 | 最简单基线,能暴露算法本身带来的增益 | | GPTQ | 近似二阶信息与逐列误差补偿 | 是 | 逐层重构,常用于 3/4 bit 权重 | | AWQ | 根据激活统计保护重要权重通道 | 是 | 不做反向传播,强调硬件友好的 weight-only | | AQLM 等 codebook 方法 | 多码本重构权重 | 是 | 更低 bit/weight,但 runtime 和 Kernel 更专用 | Weight-only 的主要收益是: - Checkpoint 和常驻权重变小; - Decode 读取权重的流量下降; - 能容纳更大 Batch 或把模型放到更少 GPU。 它不自动意味着 Prefill 低精度计算。某些 Kernel 会读取 INT4/FP4 权重后在片上解包,与高精度激活完成矩阵乘;具体收益取决于 Kernel。 ### 1.2 Weight + Activation PTQ 激活是动态张量,并且更容易出现 outlier,因此 W8A8/W4A8 比 weight-only 更依赖分布处理。 | 方法 | 核心控制点 | 目标 | |---|---|---| | SmoothQuant | 等价缩放,把 activation outlier 难度迁移到权重 | 让 INT8 W8A8 更容易量化 | | QQQ 类 W4A8 | smoothing + 权重误差补偿 | 同时降低权重和激活位宽 | | FP8 recipe | E4M3/E5M2 + current/delayed/block scaling | 使用 FP8 Tensor Core | | NVFP4 recipe | E2M1 + block/global hierarchical scaling | 在 Blackwell 上实现更低位混合精度 | 这里的重点不是“位宽更低一定更快”,而是两个 operand 是否能直接进入目标低精度矩阵乘。 ### 1.3 Rotation 与 Outlier 重排 QuaRot 等方法利用等价旋转改变 hidden state 和权重的数值分布,在不改变高精度函数的前提下减少 outlier,使权重、激活和 KV 更容易量化。 这类方法的验收不能只看 PPL: - Rotation 是否被离线吸收到权重? - 运行时是否增加额外 Hadamard/rotation Kernel? - 新增 Kernel 是否抵消低精度收益? - 长上下文和 Attention 敏感路径是否仍然合格? ### 1.4 QAT QAT 在训练或微调时模拟低精度前向,让模型参数适应量化误差。它适合 PTQ 难以维持质量的极低位场景,但成本包括: - 训练数据和训练算力; - 稳定的 fake-quant / STE 或低精度训练配方; - 与目标 Kernel 一致的 Scale、block 和累加假设; - 重新完成模型质量与安全评测。 QAT 的优势是模型可以适应噪声,不代表任意 QAT Checkpoint 都能被目标引擎高效执行。 ### 1.5 KV Cache 量化 KV quant 改变的是长期状态,不是模型权重。它的实验身份至少包括值格式、Scale 粒度与来源、敏感层例外,以及 Attention backend 是否直接消费低精度状态。`kv_cache_dtype=fp8` 只是入口配置,不能独自定义可比较 Case。 ## 2. 方法名不能直接排序 “AWQ 一定优于 GPTQ”或“FP8 一定无损”都不是稳定结论。量化质量是多变量函数: $$ Q_{\mathrm{loss}} = f( \text{model}, \text{task}, \text{bitwidth}, \text{granularity}, \text{calibration}, \text{excluded layers}, \text{runtime} ) $$ 至少要固定: - 模型、revision、tokenizer 与 chat template; - 量化对象、格式、group/block size、zero-point; - 校准数据的来源、数量、长度和随机种子; - 不量化的层; - 推理引擎、Kernel backend 和硬件; - 解码参数和评测数据。 论文中的某个速度或质量数字只对论文的 Case 成立,不能直接成为另一个模型或引擎的预期收益。 ## 3. G1:执行真实性 第一道门不是“能启动”,而是证明运行时执行了预期路径。 ### 需要保留的证据 | 证据 | 回答什么问题 | |---|---| | Checkpoint config | 声明了什么 quantization recipe | | Engine config readback | 引擎实际接受了哪些配置 | | Load log | 是否重打包、展开、跳过层或 fallback | | Kernel trace | operand dtype、Q/DQ、cast 与实际 Kernel | | HBM readback | 常驻权重是否仍是压缩格式 | 建议为每个实验输出一条规范化身份: ```text model@revision + quant_recipe@revision + engine@version + kernel_backend + gpu_arch + TP/PP/EP ``` 若 Kernel 路径无法证明,结果只能标记为“模型可加载”,不能标记为“低精度执行已验证”。 CPU 与 GPU 的工具不同,但都要从配置声明走到实际指令、数据移动和端到端结果;完整的 perf/IBS → GPU trace 映射由[CPU 工程师理解量化](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/)统一维护。 ## 4. G2:模型质量 ### 4.1 不要只看一个 PPL PPL 是快速回归信号,但不能代表完整能力。建议至少覆盖: | 维度 | 指标或数据 | 目的 | |---|---|---| | Language modeling | WikiText/C4 PPL | 检查整体分布拟合漂移 | | 知识与理解 | MMLU 类任务 | 检查知识和选择题能力 | | 数学/推理 | GSM8K、数学集或内部 reasoning 集 | 极低位通常更容易暴露链式误差 | | 代码 | HumanEval/MBPP 或内部代码集 | 检查精确 token 序列能力 | | 指令遵循 | IFEval 或业务指令集 | 检查格式和约束执行 | | 长上下文 | Needle、长文 QA、真实长请求 | 检查 KV/Attention 误差随长度累积 | | 业务样本 | 固定黄金集 | 决定是否满足真实上线标准 | ### 4.2 比较合同 - 高精度与量化模型使用相同 tokenizer、prompt template 和 decoding config。 - 对确定性任务固定随机种子或使用 greedy decode。 - 同时报告绝对分数和相对高精度基线的变化。 - 预先定义 acceptance budget,不能看完结果后再改阈值。 - 把“未运行”“运行失败”“不支持”和“质量不达标”分开记录。 ## 5. G3:容量收益 容量结果至少拆成: $$ M_{\mathrm{peak}} = M_{\mathrm{weights}} + M_{\mathrm{scales}} + M_{\mathrm{KV}} + M_{\mathrm{activations}} + M_{\mathrm{workspace}} + M_{\mathrm{communication}} $$ 建议同时报告: - Checkpoint 大小; - 加载后常驻权重 HBM; - Scale/zero-point/padding; - KV bytes/token/rank; - 给定 Context 下可容纳的最大 Batch; - 峰值 HBM,而不是只看 steady-state; - TP/EP 下 per-rank 与 global 的区别。 如果权重下降但 workspace 或 runtime expansion 抵消了收益,应分别展示,而不是只给一个“节省百分比”。 ## 6. G4:性能收益 ### 6.1 Case 身份 性能比较必须固定以下字段: ```yaml model: exact checkpoint and revision quantization: exact recipe and excluded layers engine: name, version, commit, kernel backend hardware: GPU SKU, count, topology, power mode parallelism: TP, PP, EP, DP workload: ISL, OSL, batch/concurrency, KV hit ratio serving: scheduler, prefix cache, chunked prefill, speculative decoding slo: TTFT and TPOT constraints ``` ### 6.2 指标分层 | 层级 | 指标 | 用途 | |---|---|---| | Capacity | 峰值 HBM、最大 Batch | 量化是否释放了可用容量 | | End-to-end | TTFT、TPOT、TPS/TPM、请求吞吐 | 产品是否真正受益 | | Phase | Prefill/decode time、queue wait | 收益发生在哪个阶段 | | Kernel | GEMM/BMM/Attention/Q/DQ/cast 时间 | 解释为何达到或没达到理论值 | | Cost | GPU 数、功耗、tokens/成本 | 决定生产价值 | ### 6.3 Sweep 而不是单点 至少进行: - ISL/OSL 分桶; - Batch 或并发 sweep; - 冷/热 KV Cache; - Prefill 与 Decode 分阶段; - 一个高精度基线和一个简单 RTN 基线; - 多次重复并报告分位数。 单个 `batch=1` TPS 不能代表吞吐,单个大 Batch 点也不能代表低延迟。 ## 7. 推荐实验矩阵 | 变量 | 最小集合 | |---|---| | Precision | BF16、FP8/W8A8、W4A16、目标 W4A8/FP4 | | Method | RTN、一个 weight-only PTQ、一个 activation-aware/W+A 方案 | | Workload | 短 Prefill、长 Prefill、小 Batch Decode、大 Batch Decode | | Context | 2K、8K、32K 或业务 P50/P95 | | Quality | PPL + 2 个关键公开任务 + 业务黄金集 | | Runtime | 至少记录 engine/kernel 版本与 trace 证据 | 最终结果表不应只写一个 `speedup`: | Case | Runtime verified | Quality | Peak HBM | TTFT | TPOT | TPS | Result state | |---|---|---|---:|---:|---:|---:|---| | BF16 baseline | yes | baseline | — | — | — | — | comparable | | Quant recipe A | yes/no | Δ | — | — | — | — | comparable / fallback | | Quant recipe B | yes/no | Δ | — | — | — | — | unsupported / failed | `unsupported`、`failed`、`missing` 和 `not_comparable` 都不能填成 0。 ## 8. 选型边界 | 目标 | 优先研究 | 首要风险 | |---|---|---| | 小 Batch Decode / 显存不足 | W4A16、W4A8、KV8 | Kernel 解包、fallback、质量 | | Prefill 吞吐 | FP8/W8A8/W4A8 | 激活 outlier、Tensor Core 命中 | | 极低位端到端 | Rotation、QAT、FP4 recipe | 训练成本、Attention/KV 稳定性 | | CPU/边缘 | GGUF/CPU 专用格式 | GPU 结论不可迁移 | | 长上下文 | KV FP8、敏感层保留高精度 | Scale 校准与误差累积 | | 生产 serving | 引擎原生 recipe + 可追溯 Kernel | 版本兼容矩阵快速变化 | 框架支持是动态事实。以 vLLM 为例,AWQ、GPTQ、Marlin、FP8、bitsandbytes、GGUF 和不同硬件的兼容矩阵会随版本变化;正式部署应锁定版本并保存配置回读,不在文章里固化成永久能力表。 ## 相关页面 - [CPU 工程师理解量化](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/) — perf/IBS 与 GPU trace 的证据映射 - [量化基础](https://weigao.cc/ai-systems/llm-inference/03-quantization/) — 对象、格式、Scale 与性能边界 - [FP4/FP8 量化](https://weigao.cc/ai-systems/llm-inference/fp4-fp8-quantization/) — Current/Delayed/MXFP8/NVFP4 的 Scale 合同 - [GLM-5.2 量化执行图](https://weigao.cc/ai-systems/llm-inference/glm52-operator-quantization/) — 单模型算子级混合精度案例 - [Compute-bound vs Memory-bound](https://weigao.cc/ai-systems/llm-inference/02-compute-vs-memory-bound/) — 用 Roofline 解释不同阶段的收益 - [Profiling 到 Simulation](https://weigao.cc/ai-systems/profiling/profiling-to-simulation-evidence-chain/) — 从运行证据到性能模型 ## 参考资料 - [GPTQ](https://arxiv.org/abs/2210.17323) - [AWQ](https://arxiv.org/abs/2306.00978) - [SmoothQuant](https://arxiv.org/abs/2211.10438) - [QuaRot](https://arxiv.org/abs/2404.00456) - [vLLM Quantization](https://docs.vllm.ai/en/latest/features/quantization/) - [vLLM Quantized KV Cache](https://docs.vllm.ai/en/latest/features/quantization/quantized_kvcache/) --- ## AMX 指令 > Source: https://weigao.cc/cpu-gpu/cpu-architecture/instruction-sets/amx/ > Date: 2026-07-26 > Tags: x86, architecture, Computer Architecture ## 1. BF16 其结构如下图所示: ![BF16 的 sign、exponent 与 mantissa 位布局](https://weigao.cc/docs/cpu-gpu/cpu-architecture/images/amx-1750062373190.png) ## 2. AMX 的执行模型 AMX 的核心是把矩阵分块放进二维 Tile register,再由 TMUL 执行矩阵乘累加。第一代 AMX 包含 `AMX-TILE`、`AMX-INT8` 和 `AMX-BF16`;具体处理器支持哪些扩展,仍需通过 CPUID、操作系统和运行库核对。 - Tile register file 包含 `TMM0` 到 `TMM7`。 - 每个 Tile 最大为 **16 行 × 64 byte = 1 KiB**。 - AMX-INT8 使用 INT8 operand,并累加到 INT32。 - AMX-BF16 使用 BF16 operand,并累加到 FP32。 这体现了低精度矩阵计算的通用合同: ```text 窄精度输入 → 专用 tile 矩阵乘 → 更宽精度累加 → 高精度或目标格式输出 ``` 它与 GPU Tensor Core 的数学结构相似,但并行规模、存储层次、数据布局和 Kernel 调度不同。面向 LLM 量化的完整对照见[从 AVX/AMX 到 Tensor Core](https://weigao.cc/ai-systems/llm-inference/cpu-engineer-quantization-bridge/)。 ## 参考资料 - [Intel AMX Overview](https://www.intel.com/content/www/us/en/products/docs/accelerator-engines/what-is-intel-amx.html) - [Intel AMX INT8 Code Sample](https://www.intel.com/content/www/us/en/developer/articles/code-sample/advanced-matrix-extensions-intrinsics-functions.html) --- ## Attention 架构演化:从多头注意力(MHA)到 GQA、MLA > Source: https://weigao.cc/ai-systems/llm-inference/attention-evolution/ > Date: 2026-07-21 > Tags: llm-inference, attention, kv-cache 现代 LLM 没有沿一条路线简单“替代 MHA”。更稳定的理解方式是看四个控制点:存几份 KV、每份表示多大、每步访问多少历史,以及是否仍保存逐 token KV。 :::important[30 秒复习] - **一句话**:Attention 演化是在表达能力、KV 容量、历史扫描和硬件效率之间重新分配成本。 - **三个判断**:MQA/GQA 减少 KV 份数;MLA 压缩 KV 表示;稀疏/局部与递推结构分别减少访问范围和逐 token 历史存储。 - **核心模型**:`KV 共享 → 表示压缩 → 访问稀疏 → 递推状态` 是四条正交坐标,可被组合而非互斥替代。 - **边界**:FlashAttention 主要改变精确 attention 的实现;GQA、MLA、稀疏和递推结构改变模型参数化或可见区域,不能都称为“等价加速”。 ::: ![Attention 从 MHA 到混合架构的演化地图](https://weigao.cc/docs/ai-systems/llm-inference/images/attention-evolution.svg) ## 1. 基线:缩放点积注意力与 MHA 单头缩放点积注意力为: $$ \operatorname{Attention}(Q,K,V) = \operatorname{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}}\right)V $$ Query 表示“当前在找什么”,Key 用于匹配,Value 是匹配后取回的内容。MHA 用多组投影并行学习不同表示子空间: $$ \operatorname{MHA}(X) =\operatorname{Concat}(\operatorname{head}_1,\ldots,\operatorname{head}_h)W^O $$ 逻辑上的多个 head 通常由融合 GEMM 一次生成 Q/K/V 后再 reshape;它不意味着物理上执行许多独立小矩阵乘法。`W^O` 是输出投影,也不是 softmax attention weight。 MHA 的服务成本集中在三处: | 阶段 | 主要成本 | 随上下文增长 | |---|---|---| | Prefill | Query 与可见 Key 的交互 | dense causal attention 约为 $O(S^2)$ | | Decode | 每步读取历史 K/V | 每步约为 $O(S)$ | | 常驻资源 | 每层、每 token 的 K/V | 容量约为 $O(S)$ | ## 2. 四条演化路线 | 路线 | 控制点 | 代表结构 | 主要收益 | 主要代价 | |---|---|---|---|---| | KV 共享 | 存几份 K/V | MQA、GQA | 降低 cache 容量与读取量 | 约束 KV 参数化 | | 表示压缩 | 每份 KV 多大 | MLA | 降低 bytes/token | 需要专用投影、layout 和 kernel | | 访问稀疏 | 每步看多少历史 | Sliding Window、Block/Routed Sparse | 降低长上下文交互 | 可能漏掉远距离信息 | | 递推状态 | 是否保存逐 token KV | Linear/Recurrent Attention | decode 读写固定状态 | 有限状态会压缩历史细节 | 这些路线可以混合。例如一套模型可以在多数层使用局部或递推结构,在少数层保留完整 GQA/MLA,并在系统层继续做分页和量化。 ## 3. MQA 与 GQA:减少 KV 份数 设 Query heads 数为 $h$,KV heads 数为 $g$: | 架构 | Query heads | KV heads | 共享方式 | |---|---:|---:|---| | MHA | $h$ | $h$ | 每个 Q head 有独立 K/V | | GQA | $h$ | $g$,且 $1