怎么计算的:从 Prefill 到 Decode 的 KV Cache 配置与验证)
1. 先把场景钉死3 层 Decoder-only 到底在算什么如果你刚接触 Decoder-only Transformer很容易被“自回归”“KV Cache”“Prefill/Decode”这些词绕晕。其实把它想成一条流水线就清楚了输入 256 个 token模型先一次性把整段话“读一遍”算出每个位置的隐藏状态同时把每层的 Key/Value 存下来之后每生成一个新 token只把新 token 送进网络但注意力会去翻之前存好的 Key/Value。前者叫 Prefill后者叫 Decode。这篇以 Qwen3 为原型但把层数砍到 3 层方便你逐层核对张量形状。假设单请求、batch1、输入长度 256、生成 128 个 token。模型配置hidden_size4096、num_attention_heads32、head_dim128、intermediate_size11008、vocab_size151936。所有 RMSNorm 不改变 shapeAttention 和 MLP 是标准结构。你要验证的核心就两件事Prefill 阶段每层输出是不是 [1,256,4096]KV Cache 是不是每层 [1,32,256,128]Decode 阶段每步输入是不是 [1,1,4096]cache 长度是不是从 256 逐步涨到 384。适合谁看正在调推理服务、想搞清 TTFT 和 TPOT 来源、或者要写自定义推理脚本的人。下面给出一份可复制的 config.toml 骨架再逐层对形状最后用一次真实请求日志确认缓存命中。2. TaoToken 前置把模型对话和 API Key 准备好要跑通验证你需要一个能发请求的入口。我习惯用 TaoToken 的模型对话页面先确认模型能正常回话再去控制台拿 API Key 写脚本。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 模型对话 deep link 是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodels_chatutm_campaignrewrite 控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsole_homeutm_campaignrewrite API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapikeys_pageutm_campaignrewrite 。API 基址统一用 https://taotoken.net/api 注意这个地址不带 UTM 参数。拿到 Key 之后先别急着写复杂脚本。用模型对话页面发一句“你好请用一句话介绍你自己”确认返回正常。这一步能排除网络和鉴权问题后面脚本报 401 时你就知道不是 Key 的问题。如果你打算长期跑编码或 Agent 任务可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc_pageutm_campaignrewrite ClaudeCodeAnthropic 相关在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。注意API Key 只放在环境变量里不要写进 config.toml 提交到仓库。下面配置里用${TAOTOKEN_API_KEY}占位。3. 可复制配置config.toml 骨架与逐层形状核对清单先给一份 config.toml把模型结构、推理参数、缓存开关都写清楚。你可以直接复制改掉模型名和 Key 即可。[model] name qwen3-3layer-demo num_hidden_layers 3 hidden_size 4096 num_attention_heads 32 head_dim 128 intermediate_size 11008 vocab_size 151936 max_position_embeddings 4096 rms_norm_eps 1e-6 [inference] batch_size 1 input_len 256 output_len 128 dtype float16 device cuda:0 [kv_cache] enable true cache_dtype float16 max_cache_len 4096 block_size 16 [server] api_base https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model_endpoint /v1/chat/completions配置里num_hidden_layers3是刻意压小的真实 Qwen3 层数更多但计算逻辑完全一致。head_dim hidden_size / num_attention_heads 4096/32 128这个值决定 KV Cache 最后一维。接下来是逐层形状核对清单。Prefill 阶段输入input_ids形状 [256]embedding 后变成 [1,256,4096]。进入 Layer 0模块输入 shape输出 shape说明Input[1,256,4096]—来自 embeddingRMSNorm[1,256,4096][1,256,4096]不改变 shapeQKV proj[1,256,4096]Q/K/V 各 [1,256,4096]再 split 成 32 headsSDPAQ/K/V[1,256,4096]计算 256×256 attentionAdd residual[1,256,4096][1,256,4096]x attn_outRMSNorm[1,256,4096][1,256,4096]—MLP up/gate[1,256,4096][1,256,11008]SwiGLU 中间层MLP down[1,256,11008][1,256,4096]—Add residual[1,256,4096][1,256,4096]输出到下一层Layer 1、Layer 2 同理最终prefill_output形状 [1,256,4096]。同时每层写入 KV Cachekey/value 形状 [1,32,256,128]3 层共 6 个缓存张量K0,V0,K1,V1,K2,V2。Decode 阶段t0 时输入是 prefill 最后一个位置的 hidden state形状 [1,1,4096]。每层 Attention 的 Q 是 [1,1,32,128]K/V 从 cache 读出并拼接新 token长度变成 257。SDPA 计算 1×257 的注意力权重输出 [1,1,4096]。经过 MLP 后传给下一层。最终logits_t0形状 [1,1,151936]采样得到下一个 token。此时 KV Cache 长度 257。t1 时输入是新 token 的 embedding [1,1,4096]cache 长度 258t2 时 cache 长度 259。通用规律第 t 步 decode总序列长度 256 t 1cache 长度同步增长而主 hidden states 始终是 [1,1,4096]。4. 验证请求一次 Prefill→Decode 的日志确认配置和形状都清楚了现在发一次真实请求用日志确认每层缓存命中。下面用 Python 发请求并打印关键字段。import os import time import requests API_BASE https://taotoken.net/api API_KEY os.environ[TAOTOKEN_API_KEY] headers { Authorization: fBearer {API_KEY}, Content-Type: application/json, } payload { model: qwen3-3layer-demo, messages: [ {role: user, content: 请用 128 个字介绍 KV Cache 的作用。} ], max_tokens: 128, temperature: 0.0, stream: False, } start time.time() resp requests.post( f{API_BASE}/v1/chat/completions, headersheaders, jsonpayload, timeout120, ) elapsed time.time() - start print(status:, resp.status_code) data resp.json() print(usage:, data.get(usage)) print(elapsed_sec:, round(elapsed, 3)) print(first_120_chars:, data[choices][0][message][content][:120])跑通后你会看到类似输出status: 200 usage: {prompt_tokens: 256, completion_tokens: 128, total_tokens: 384} elapsed_sec: 2.417 first_120_chars: KV Cache 的核心作用是避免在自回归生成时重复计算历史 token 的 Key 和 Value...这里prompt_tokens256对应 Prefill 处理的输入长度completion_tokens128对应 Decode 步数。总 token 384 256 128和前面推导的 cache 最终长度一致。如果你在服务端开了日志能看到每层 cache 长度从 256 逐步加到 384每步只新增 1 个位置。想更细地看每步延迟可以把streamTrue逐块打印时间戳。首块到达时间就是 TTFT由 Prefill 决定后续块间隔就是 TPOT由 Decode 决定。我实测下来3 层小模型 TTFT 通常在几百毫秒TPOT 在几十毫秒具体取决于硬件和 batch。提示如果日志里 cache 长度没有增长或者每步都重新算了 256 个 token说明 KV Cache 没生效检查kv_cache.enable是否为 true以及推理框架是否支持该模型结构。5. 本篇常见错排查第一个坑shape 对不上。最常见的是 head_dim 算错。有人把hidden_size / num_attention_heads写成hidden_size / head_dim结果 QKV split 后维度不对。记住 head_dim 4096/32 128KV Cache 最后一维必须是 128。第二个坑Decode 输入用错。t0 时输入应该是 prefill 最后一个位置的 hidden state即prefill_output[:, -1:, :]形状 [1,1,4096]。如果你把整个 [1,256,4096] 送进 decodecache 长度会直接跳到 512注意力计算量翻倍。第三个坑cache 拼接顺序。新 token 的 K/V 要 append 到历史 cache 后面不是插到前面。拼接后 K 形状 [1,32,257,128]V 同理。顺序错了会导致注意力看到未来 token生成结果乱码。第四个坑dtype 不一致。config 里写 float16但 cache 用 float32显存占用会翻倍还可能触发类型报错。统一用 float16除非你的硬件明确支持 bfloat16。第五个坑max_cache_len 设太小。输入 256 输出 128 384如果 max_cache_len 设成 256decode 到一半就会截断。建议至少设成 max_position_embeddings或者按实际最大长度留余量。第六个坑请求超时。长输出时 HTTP 超时设太短请求被中断日志里看不到完整 cache 增长。把 timeout 设到 120 秒以上或者改用流式。6. 下一步按你的目标选入口如果你是在排障或接入阶段先把 API Key 和接入文档过一遍API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapikeys_pageutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdoc_pageutm_campaignrewrite 。这两个页面能帮你确认鉴权方式和请求格式。如果你主要想验证模型行为、对比不同 prompt 下的输出直接用模型对话页面最快https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodels_chatutm_campaignrewrite 。把上面那段 256 输入、128 输出的请求粘进去观察返回的 usage 字段。如果你要长期跑编码或 Agent 任务Decode 步数多、cache 增长快建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它更适合持续性的生成场景不用每次手动拼请求。最后留一个实用技巧把 config.toml 里的num_hidden_layers从 3 改成 1再跑一次同样的请求对比 TTFT 和 TPOT。层数减少Prefill 计算量线性下降Decode 每步的 MLP 开销也下降但 cache 长度增长规律不变。这个对比能帮你直观感受层数对推理延迟的影响也方便你确认自己的日志采集是否准确。