step_comparison.csv 口头讲解稿本文配合文件:
textE:\Code\llama.cpp_experiment\repro_20260804\repro_20260804_dynamic_4plus1\step_comparison.csv
这份 CSV 记录的是 llama.cpp 连续批处理过程中,每一个调度周期的状态。讲解时可以把每一行理解为:
“服务器在这一轮选择了哪些请求?每个请求处理了多少 token?KV Cache 发生了什么变化?这一轮花了多少时间?”
CSV 的每一行对应一个 step,也就是一次调度周期。
例如第 4 行:
textdynamic_4plus1,4,"[0,1,2,3,4]","[2,1,0,3,4]","[2,1,0,3,4]","[decode,decode,decode,decode,prefill]","[1,1,1,1,640]","[515,259,131,1027,0]","[516,260,132,1028,640]",644,"[512,132]",...,43.333
口头上可以这样说:
“这是 dynamic_4plus1 实验的第 4 个调度周期。此时一共有 5 个请求,其中前 4 个请求正在 decode,每个请求处理 1 个 token;第 5 个请求刚刚加入,正在 prefill,一次处理 640 个 token。整个逻辑批次一共 644 个 token,受 512 的 ubatch 大小限制,被拆成 512 和 132 两个小批次。这一轮总共耗时 43.333 毫秒。”
case实验名称。
本文件中都是:
textdynamic_4plus1
它表示 4 个请求先运行,第 5 个请求动态加入。
step调度周期编号,从 0 开始。
可以理解为服务器循环中的第几轮,而不是某个请求的第几个 token。
例如:
textstep=0:首次处理 prompt step=1:第一次 decode step=4:新请求加入 step=5:5 个请求全部 decode
request_ids本轮参与计算的请求编号。
例如:
text[0,1,2,3]
表示本轮只有请求 0、1、2、3。
到了第 4 周期:
text[0,1,2,3,4]
表示请求 4 已经被调度器加入。
slot_ids本轮请求对应的逻辑槽位:
textrequest 0 -> slot 2 request 1 -> slot 1 request 2 -> slot 0 request 3 -> slot 3 request 4 -> slot 4
例如第 0 周期:
textrequest_ids = [0,1,2,3] slot_ids = [2,1,0,3]
应解释为:请求 0 使用槽位 2,请求 1 使用槽位 1,而不是请求 0 使用槽位 0。两个数组按照相同位置一一对应。
seq_idsKV Cache 使用的序列编号。
本实验中它与 slot 的编号恰好相同,但概念不同。slot_id 表示服务器槽位,seq_id 用于识别 KV Cache 所属序列。
它保证不同请求虽然被放在同一 batch 中计算,但不会互相读取上下文。
states表示每个请求本轮的处理阶段:
textprefill:处理输入 prompt decode:根据上下文生成后续 token
数组与 request_ids 一一对应。
例如:
textrequest_ids = [0,1,2,3,4] states = [decode,decode,decode,decode,prefill]
表示请求 0~3 继续生成,第 5 个请求刚开始处理 prompt。
query_lens每个请求在本轮送入模型的 token 数量。
第 0 周期:
text[512,256,128,1024]
表示 4 个请求分别处理对应长度的 prompt。
第 1 周期:
text[1,1,1,1]
表示每个请求各处理 1 个 decode token。
第 4 周期:
text[1,1,1,1,640]
表示旧请求各处理 1 个 token,新请求一次处理 640 个 prompt token。
kv_before 和 kv_after分别表示本轮开始和结束时,每个请求拥有的 KV Cache 长度。
第 4 周期:
textkv_before = [515,259,131,1027,0] kv_after = [516,260,132,1028,640]
逐项解释:
textrequest 0:515 -> 516,新增 1 个 decode token request 1:259 -> 260,新增 1 个 decode token request 2:131 -> 132,新增 1 个 decode token request 3:1027 -> 1028,新增 1 个 decode token request 4:0 -> 640,完成 640 个 prompt token 的 prefill
因此可以用 kv_after - kv_before 判断本轮每个请求实际写入了多少 KV。
logical_batch_tokens本轮所有请求要处理的 token 总数。
第 0 周期:
text512 + 256 + 128 + 1024 = 1920
所以:
textlogical_batch_tokens = 1920
第 4 周期:
text1 + 1 + 1 + 1 + 640 = 644
所以:
textlogical_batch_tokens = 644
ubatch_tokens表示 logical batch 被拆成了哪些物理小批次。
第 0 周期:
textlogical batch = 1920 ubatch_tokens = [512,512,512,384]
第 4 周期:
textlogical batch = 644 ubatch_tokens = [512,132]
口头解释为:
“逻辑上这一轮需要处理 644 个 token,但单个物理批次最多容纳 512 个,所以先执行 512 个,再执行剩下的 132 个。”
mask_shapes表示 attention mask 的形状,记录模型本轮如何组织 query token 和可见 KV 范围。
例如第 0 周期:
text[[512,512,1,1], [1024,512,1,1], [1536,512,1,1], [2048,384,1,1]]
重点看前两个数字:
所以 [1024,512,1,1] 不是说本轮输入了 1024 个 query,而是说本轮 query 为 512 个,但 attention 可以看到的 KV 范围已经达到 1024。
第 4 周期:
text[[2560,512,1,1],[2816,132,1,1]]
这对应 644 个 token 被拆成 512 和 132 两个 ubatch。
这些字段的单位都是毫秒:
| 字段 | 口头解释 |
|---|---|
batch_build_latency_ms | 组织本轮 batch 的时间 |
kv_allocate_latency_ms | 分配 KV Cache 空间的时间 |
mask_build_latency_ms | 构造 attention mask 的时间 |
graph_build_latency_ms | 构造或准备计算图的时间 |
ubatch_compute_latency_ms | 执行一个或多个 ubatch 的模型计算时间 |
decode_call_latency_ms | 调用 decode 执行接口的整体时间 |
sampling_latency_ms | 根据 logits 采样下一个 token 的时间 |
tensor_dump_latency_ms | 导出 tensor 用于调试跟踪的时间 |
step_total_latency_ms | 这一整个调度周期的总耗时 |
这些时间不一定简单相加,因为部分操作可能包含内部调用、同步或统计开销。实际性能比较时,优先关注 step_total_latency_ms,并区分 prefill 周期和 decode 周期。
textstates = [prefill,prefill,prefill,prefill] query_lens = [512,256,128,1024] kv_before = [0,0,0,0] kv_after = [512,256,128,1024] logical = 1920 ubatches = [512,512,512,384] total = 200.751 ms
口头讲解:
“服务器先同时接收 4 个请求。因为它们都还没有上下文,所以状态全是 prefill。四个 prompt 总共 1920 个 token。由于单个 ubatch 只能处理 512 个 token,实际分成 4 次计算。这一轮结束后,4 个请求的 KV 长度分别变成 512、256、128 和 1024。因为这是大规模 prompt 计算,所以耗时最高,为 200.751 毫秒。”
textstates = [decode,decode,decode,decode] query_lens = [1,1,1,1] kv_before = [512,256,128,1024] kv_after = [513,257,129,1025] logical = 4 ubatches = [4] total = 15.052 ms
口头讲解:
“第 0 周期已经完成 prompt,因此现在 4 个请求都进入 decode。每个请求本轮只输入 1 个 token,总共 4 个 token,一个 ubatch 就能完成。每个请求的 KV 长度增加 1,周期耗时下降到 15.052 毫秒。”
第 2 周期:
textkv_before = [513,257,129,1025] kv_after = [514,258,130,1026] total = 13.061 ms
第 3 周期:
textkv_before = [514,258,130,1026] kv_after = [515,259,131,1027] total = 10.213 ms
口头讲解:
“这两轮没有新请求加入,4 个请求每轮各生成一个 token。所以每一轮的 query 长度都为
[1,1,1,1],KV 长度逐项加 1,batch 大小保持为 4。”
textstates = [decode,decode,decode,decode,prefill] query_lens = [1,1,1,1,640] kv_before = [515,259,131,1027,0] kv_after = [516,260,132,1028,640] logical = 644 ubatches = [512,132] total = 43.333 ms
口头讲解:
“这一轮可以看到连续批处理最核心的行为。原来的 4 个请求没有停下来,它们继续 decode,每个处理 1 个 token。同时第 5 个请求加入,状态是 prefill,需要处理 640 个 prompt token。于是本轮总共处理 644 个 token,拆成 512 和 132 两个 ubatch。旧请求的 KV 各增加 1,而新请求的 KV 从 0 直接建立到 640。”
textstates = [decode,decode,decode,decode,decode] query_lens = [1,1,1,1,1] kv_before = [516,260,132,1028,640] kv_after = [517,261,133,1029,641] logical = 5 ubatches = [5] total = 27.659 ms
口头讲解:
“第 4 周期已经完成了请求 4 的 prompt 处理,所以第 5 周期 5 个请求全部进入 decode。每个请求各处理一个 token,总共 5 个 token,一个 ubatch 就够。请求 4 的 KV 从 640 增加到 641,说明它现在已经和其他请求一样进入逐 token 生成阶段。”
可以直接按照下面这段话讲解:
“我们先看第 0 行。这里有 4 个请求,状态全部是 prefill,说明服务器正在处理输入 prompt。它们的长度是 512、256、128 和 1024,加起来一共 1920 个 token。因为 ubatch-size 是 512,所以被拆成 512、512、512 和 384 四个物理批次。处理完成后,KV Cache 长度分别变成对应的 prompt 长度。
接着看第 1 到第 3 行,状态全部变成 decode。每个请求每轮只处理一个 token,所以 query_lens 是
[1,1,1,1],KV 长度每轮分别增加 1,batch 中一共只有 4 个 token,执行时间也明显下降。再看第 4 行,这是动态加入请求的地方。此时请求 0 到 3 继续 decode,而请求 4 进入 prefill。前四个请求各处理 1 个 token,第五个请求处理 640 个 prompt token,所以总数是 644。这个 batch 又被拆成 512 和 132 两个 ubatch。旧请求的 KV 只增加 1,新请求的 KV 从 0 建立到 640。
最后看第 5 行,五个请求的状态已经全部是 decode,query_lens 变成五个 1,说明新请求已经完成 prefill,开始和其他请求一起逐 token 生成。整个过程中,batch 是合并计算的,但每个请求的 slot、seq_id、KV Cache 和 attention 范围仍然独立。”
states 从 prefill 变成 decode,表示请求从处理 prompt 进入生成阶段。kv_after 通常等于 kv_before + query_lens,它反映本轮新增的 KV Cache。logical_batch_tokens 是逻辑总量,ubatch_tokens 是受容量限制后实际执行的拆分结果。本文作者:WarF
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!