从一次 generate() 开始:理解 vLLM-Ascend 的推理过程
从一次 generate() 开始:理解 vLLM-Ascend 的推理过程
输入一句话,调用 generate(),模型就返回了后续文本。但在推理框架内部,这个调用会被拆成多轮工作:调度请求、准备输入、读写 KV Cache、执行模型、选择新 token,最后释放请求占用的缓存块。
本文用 Qwen3-0.6B 在昇腾 NPU 上的一组实验,把这条链路连起来。读者只需要了解基本 Python;文中会逐步解释 Tensor 形状、缓存地址和采样的含义。
先记住本文要追踪的例子:
输入:The capital of France is
输出: Paris. The
我们不评价这句话生成得好不好,而是追问:这三个新 token 是怎样算出来的,计算期间的数据存在哪里,请求结束后又发生了什么?
1. 实验起点:一次调用,三个新 token
本文主线采用同步调度、关闭 Prefix Cache 的观察实验。最早的启动测试使用过不同配置,已保留在实验原稿中,不与下面的逐轮记录混用。
| 项目 | 实验环境 |
|---|---|
| 硬件 | 昇腾 A3,使用一个可见逻辑设备 |
| 模型 | Qwen3-0.6B,模型目录 /workspace/models/Qwen3-0.6B |
| PyTorch / torch-npu | 2.10.0+cpu / 2.10.0.post4 |
| vLLM / vLLM-Ascend | 0.26.0+empty / 0.26.0rc1 |
| 执行方式 | Eager、同步调度、TP=1 |
| 跨请求前缀复用 | 关闭 |
下面的脚本展示主线配置及最终输入输出。逐轮 Tensor 和缓存记录来自额外加入的观察点,仅运行这段脚本不会打印本文全部中间结果。它要求环境中已经安装匹配的软件,并准备好模型文件。
from vllm import LLM, SamplingParams
llm = LLM(
model="/workspace/models/Qwen3-0.6B",
tensor_parallel_size=1,
max_model_len=1024,
max_num_seqs=1,
gpu_memory_utilization=0.3,
enforce_eager=True,
async_scheduling=False,
enable_prefix_caching=False,
)
params = SamplingParams(
temperature=0,
max_tokens=3,
ignore_eos=True,
)
result = llm.generate(["The capital of France is"], params)[0]
output = result.outputs[0]
print("输入 token IDs:", result.prompt_token_ids)
print("输出 token IDs:", output.token_ids)
print("生成文本:", repr(output.text))
print("结束原因:", output.finish_reason)
结果为:
输入 token IDs: [785, 6722, 315, 9625, 374]
输出 token IDs: [12095, 13, 576]
生成文本: ' Paris. The'
结束原因: length
Tokenizer 把文本编码为 token ID。这些整数是词表编号,不是向量,也不是字符的 ASCII 编码;一个 token 可以对应词、词片段、标点等。
这里使用文本续写,没有套用聊天模板。max_tokens=3 限制的是三个新生成的 token,不包含输入;因此输出可以停在半句话中。temperature=0 使用贪心选择,ignore_eos=True 则让本实验忽略 EOS 停止条件,便于观察长度限制。
还有三组配置容易混淆:
- Eager 与同步调度分别涉及模型执行方式和调度方式,不是同一个开关。
- 关闭 Prefix Cache表示本实验不复用其他请求的前缀缓存;同一个请求在 Decode 时仍然使用 KV Cache。
gpu_memory_utilization在这个 NPU 环境中仍沿用该参数名,它参与显存预算设置,不表示把计算速度限制为 30%。
2. 先把整条链路放在一起
下面是本文的阅读地图。它表示职责和数据流,不要求每个箭头都对应一次直接的 Python 函数调用。
文本 → Tokenizer → 请求
↓
Engine 推进请求
↓
Scheduler 决定本轮计算哪些 token
↓
Worker → ModelRunner
↓
准备输入 Tensor 和 KV 地址信息
↓
模型计算:Embedding、Attention、MLP 等
↓
选取 hidden states → logits → 采样
↓
新 token 写入请求状态
↓
未结束:进入下一轮
已结束:返回结果、归还缓存块
在本实验中,vLLM 提供请求管理、调度、模型实现等通用组件;vLLM-Ascend 提供或适配 NPU 执行相关的 Worker、Runner、Attention、算子等。边界并非完全互不接触:后文会看到,Ascend 也通过补丁扩展了模型和调度路径。
其中 Scheduler 决定“本轮做什么”,Runner 把任务转成模型能执行的数据。一份请求可能经历很多轮调度,一次 generate() 也不等于一次模型前向计算。
3. 先追第一个输出:模型接收到什么?
第 1 节已经得到输出 Paris. The,但最终结果没有告诉我们中间发生了什么。先缩小观察范围:只追第一个新 token 12095 是怎样从输入句子算出来的。 后面的两轮先放在一边。
Tokenizer 已经把输入句子转换成五个编号:
The capital of France is
↓
[785, 6722, 315, 9625, 374]
这时有了文本的数字表示,模型计算却还没有开始。请求进入框架后,Scheduler 决定本轮处理哪些 token;在这次短输入实验中,五个输入 token 被安排在同一轮处理。首次处理提示词的阶段称为 Prefill。
Worker 接收这份调度结果,Runner 再准备模型需要的输入。我们在输入准备之后设置观察点,首先看两个字段:
input_ids = [785, 6722, 315, 9625, 374]
positions = [0, 1, 2, 3, 4]
input_ids 表示输入的内容编号,positions 表示这些内容在序列中的位置。例如 374 是一个 token ID,4 表示它位于输入序列的第 5 个位置。两者逐项对应,不能混为一类数字。
源码入口是 vllm_ascend/worker/model_runner_v1.py 中的输入准备与模型执行路径。Runner 还会准备请求边界、缓存地址等辅助信息;它们的作用将在批处理和 KV 读写时展开。现在先沿着这五个输入往下走。
到这里,框架已经把“处理这句话”变成了“计算这五个位置”。但这五个整数还不是模型用来预测的特征,下一步要进入模型本身。
4. 五个整数进入模型后,发生了什么?
模型先通过 Embedding 按 token ID 查出向量。本次每个 token 对应 1024 个数,五个 token 就得到形状为 [5,1024] 的 Tensor:5 行,每行 1024 个数。
这一步把编号变成了可计算的特征,但仅仅查表还没有让不同位置交换上下文信息。为了根据整段提示词预测后续内容,模型还要经过多层 Transformer,让每个位置的表示结合它能看到的前文。
其中,Attention 负责按内容建立位置之间的联系。当前位置生成 Q,用它与可见位置的 K 计算匹配分数,再据此加权汇总 V。例如最后一个输入位置需要结合前面的上下文,而不只是使用自身的 Embedding。Q/K/V 都是由当前层的输入投影得到的向量,不是三份 token ID。
因此,接下来观察第一层 Attention,是为了看清这五行特征如何开始融合上下文,也为后面的 KV Cache 找到来源。以下用 N 表示本轮输入 token 数;当前 Prefill 中 N=5。
本模型有 28 层 Transformer,隐藏维度为 1024。实验在第 0 层设置观察点,记录到:
| 步骤 | 输出或关键形状 | 含义 |
|---|---|---|
| Embedding | [N,1024] | 按 token ID 查出向量 |
| 输入 RMSNorm | [N,1024] | 归一化隐藏特征 |
| QKV 投影 | [N,4096] | 一次线性投影得到待拆分的 Q/K/V |
| Q | [N,16,128] | 16 个 Query 头,每头 128 维 |
| K、V | 各 [N,8,128] | 8 个 KV 头,每头 128 维 |
| Attention 输出 | [N,2048] | 合并 16 个 Query 头的结果 |
输出投影 o_proj | [N,1024] | 映射回隐藏维度 |
表中的“头”表示 Attention 内部并行计算的多个子空间。本模型使用 GQA:16 个 Query 头对应 8 个 KV 头,每两个 Query 头共享一组 KV 头。初读时可以先关注 [N,1024] → Attention → [N,1024] 这条包含输出投影的特征流,头数用于解释中间 Tensor 的形状。
读取源码时还有一个重要发现:运行时的 Qwen3Attention.forward 被替换到了 vllm_ascend/patch/worker/patch_qwen3vl.py。文件名带 vl,但本次模型仍然是 Qwen3ForCausalLM。因此,仅阅读上游同名类还不足以确认实际执行路径。
这条路径的核心流程可简化为以下伪代码:
qkv = qkv_projection(hidden_states)
q, k, v = split_qkv(qkv)
q = q_norm(reshape_into_heads(q))
k = k_norm(reshape_into_heads(k))
q, k = rotary_embedding(positions, q, k)
attention_output = attention(q, k, v, kv_cache, metadata)
output = output_projection(attention_output)
RoPE 把位置信息作用于 Q/K,改变其数值而不改变形状;这条路径中 V 不经过 RoPE。Attention 后,模型还会经过残差连接、归一化和 MLP,并继续后续层的计算。
这里的形状来自 Embedding 和第一层的观察点,不代表已逐层观测全部 28 层或 MLP 内部。后续各层继续更新这些特征;整个模型主干完成后,实测输出仍是 [5,1024],称为 hidden_states。
现在我们已经从五个整数走到了五行融合上下文的特征,但仍没有得到 12095。下一步要看框架如何把这些特征转换成一个词表编号。
5. 模型输出怎样变成新 token?
模型主干的输出是 hidden_states,还不是文本。本文模型对应的 vllm/model_executor/models/qwen3.py 中,Qwen3ForCausalLM.forward 返回主干算出的隐藏特征。
Prefill 得到 [5,1024]:五个输入位置各有一个 1024 维表示。为了预测下一 token,Runner 执行:
sample_hidden_states = hidden_states[logits_indices]
logits = self.model.compute_logits(sample_hidden_states)
这两行来自本次环境的 vllm_ascend/worker/model_runner_v1.py。Prefill 的 logits_indices=[4],所以选取最后一个输入位置的表示,得到 [1,1024]。它已经融合了该位置可见的上下文,不只是最后一个词的原始向量。
compute_logits 再通过 LM head 等处理得到词表候选分数:
logits = self.logits_processor(self.lm_head, hidden_states)
return logits
第一轮选出的 token 会成为下一轮输入。后续利用历史 KV、继续处理新输入的阶段称为 Decode,具体复用方式留到下一节。先对照三轮模型输出和采样的实测结果:
| 本轮 | 主干输出 | 选中后的形状 | logits 形状 | 选出的 token ID |
|---|---|---|---|---|
| Prefill | [5,1024] | [1,1024] | [1,151936] | 12095 |
| Decode 1 | [1,1024] | [1,1024] | [1,151936] | 13 |
| Decode 2 | [1,1024] | [1,1024] | [1,151936] | 576 |
151936 是本次 logits 的候选维度,不是生成了这么多 token。每列保存一个候选编号的分数;logits 尚未归一化为概率。
Sampler 随后处理这些分数。本实验采用贪心选择,观察到返回的 ID 等于最高分候选。Ascend Sampler 普通贪心分支的核心是:
return logits.argmax(dim=-1).view(-1)
argmax 返回最高分所在的列编号,不是分数本身。三轮最高分分别为 17.5、19.25、17.0,对应 ID 12095、13、576。这些数不是概率,也不能直接跨轮比较“置信度”。实现还存在其他分支,这段代码不是对所有采样配置的概括。
把 Prefill 的变化连起来就是:
5 个 token IDs
→ [5,1024] 隐藏特征
→ 取第 4 行,得到 [1,1024]
→ [1,151936] 候选分数
→ 选择 token ID 12095
→ 写入请求的输出记录
→ 下一轮作为输入
这就是“一轮处理五个输入 token,却只产生一个新 token”的原因。
6. Decode 只输入一个 token,为什么还能利用整段历史?
因为前面各位置的 K/V 已保存在缓存中。Decode 1 新计算的位置是 5,但 Attention 需要的信息覆盖位置 0~5:历史部分来自缓存,当前位置的 K/V 来自本轮计算。
在本次 AscendAttentionBackendImpl 路径中,可以沿着以下位置阅读:
vllm_ascend/attention/attention_v1.py
forward
→ reshape_and_cache:把当前 K/V 写入对应槽位
→ forward_impl:选择 Attention 执行路径
缓存按块组织:block_table 记录请求使用哪些物理块,slot_mapping 指定本轮 K/V 写入块中的哪些槽位。槽位是缓存中的位置索引,不是 token ID。
第 0 层的 Key Cache 实测形状为 [1257,128,8,128],依次是块数、每块槽位数、KV 头数、每头维度。Decode 1 的槽位 133 对应这个缓存中的 [1,5,:,:]。
7. 把各轮连起来:三个输出 token 怎样产生?
现在已经看到了两个关键动作:模型为本轮输入计算并保存 K/V,采样再选出一个新 token。把它们放回请求的时间线上,就能解释为什么生成三个 token 只需要一轮 Prefill 和两轮 Decode。这个短提示词没有被拆成多个 Prefill 分块,实测为:
| 本轮 | 输入模型的 token IDs | 序列位置 positions | 本轮新生成 token |
|---|---|---|---|
| Prefill | [785,6722,315,9625,374] | [0,1,2,3,4] | 12095 |
| Decode 1 | [12095] | [5] | 13 |
| Decode 2 | [13] | [6] | 576 |
Prefill 本身就产生第一个新 token。 下一轮把这个 token 输入模型,再得到第二个;如此反复。
这里要区分“已经生成”与“已经计算过 KV”:
Prefill 结束:
已知序列 = 五个输入 token + 12095
已计算 KV 的位置 = 0~4
Decode 1 结束:
已知序列 = 五个输入 token + 12095 + 13
已计算 KV 的位置 = 0~5
Decode 2 结束:
已知序列 = 五个输入 token + 12095 + 13 + 576
已计算 KV 的位置 = 0~6
12095 刚被选出来时,还没有作为输入经过模型。它在下一轮进入模型之后,各层才计算并写入它的 K/V。最后的 576 被选出后,请求达到长度上限,本次同步实验没有再把它输入模型。
这也解释了后续把输出上限改为 5 的实验:一轮 Prefill 加四轮 Decode,得到 [12095,13,576,6722,315]。这个计数关系适用于这里的普通自回归、未拆分 Prefill、无提前停止的配置,不能直接套用于分块 Prefill 或投机解码。
到这里,再回看 Runner 的完整记录,每个字段就有了对应的计算用途:
| 数据 | Prefill | Decode 1 | Decode 2 |
|---|---|---|---|
input_ids | [785,6722,315,9625,374] | [12095] | [13] |
positions | [0,1,2,3,4] | [5] | [6] |
| 调度 token 数 | 5 | 1 | 1 |
query_start_loc | [0,5] | [0,1] | [0,1] |
seq_lens | [5] | [6] | [7] |
logits_indices | [4] | [0] | [0] |
有效 block_table | [1] | [1] | [1] |
slot_mapping | [128,129,130,131,132] | [133] | [134] |
query_start_loc 划分本轮拼接输入中的请求边界,单请求 [0,5] 表示它占据第 0~4 行;seq_lens 是本轮参与 Attention 的序列长度。logits_indices 用来选取预测行;block_table 保存逻辑块到物理块的映射,slot_mapping 则指定当前 K/V 的写入槽位。
以 Decode 1 为例,最容易混淆的三个数字是:
positions = [5] 完整序列中的位置
logits_indices = [0] 本轮只有一行,取第 0 行用于预测
slot_mapping = [133] 这一位置的 K/V 写入物理槽位 133
它们分别属于序列坐标、本轮 Tensor 行号、物理缓存地址。token ID 12095 则是内容编号,不属于上述任何一种位置。
本实验每块包含 128 个槽位。对这个普通单设备布局,地址关系为:
logical_block = position // 128
physical_block = block_table[logical_block]
slot = physical_block * 128 + position % 128
位置 5 对应块表中的第 0 项,即物理块 1,所以槽位是 1 × 128 + 5 = 133。槽位是索引,不是字节地址;块表保存映射,也不直接保存 K/V 数值。不同运行可能分到不同物理块号。
8. 使用 KV Cache,会不会改变下一 token 的计算结果?
前面已经解释了 KV 的读写方式,并串起了请求的逐轮推进过程。接下来通过一个对照实验检查:复用历史 KV,与重新计算完整上下文,会不会得到不同的下一 token 分数?我们比较两条路径:
路径 A:保留前五个位置的 KV,只输入新 token 12095
路径 B:重新输入原五个 token + 12095,完整计算六个位置
比较:两条路径预测第七个位置时的完整 logits
随后在上下文长度为 7 时再做一次。两条路径使用同一进程中的同一套模型权重,Prefix Cache 关闭。完整重算请求仍会建立自己的新 KV;这里没有把框架所有 KV 机制禁用,而是确认没有复用前一请求的前缀计算。
实际调度数量分别是缓存路径的 5、1、1,以及重算路径的 6、7。比较位置位于 compute_logits 之后、Sampler 之前:
| 上下文长度 | 每次比较的 logits 数量 | 最大绝对误差 | 均方根误差 | 两条路径最高分 token |
|---|---|---|---|---|
| 6 | 151936 | 0 | 0 | 13 |
| 7 | 151936 | 0 | 0 | 576 |
比较的是保存下来的 BF16 logits,转成 FP32 后计算误差;这种转换不能恢复 BF16 舍入前的信息。结果说明:在这两个短上下文测试点,缓存增量计算与完整重算得到的已保存 logits 一致。
这支持了 KV 复用的基本理解:因果模型中,后续 token 不会反过来改变前面位置的表示,因此可以复用历史 K/V。它不保证所有模型、精度和执行路径都逐位相等,也没有测量使用缓存带来的速度收益。
9. 两个请求怎样放进同一轮计算?
单请求链路清楚之后,再看批处理。实验把 max_num_seqs 改为 2,提交:
- A:
The capital of France is,输入 5 个 token。 - B:
Hello,输入 ID 为[9707]。
每个请求最多生成 3 个 token。实际第一轮只有 A;第二轮开始,A 的 Decode 与 B 的 Prefill 出现在同一批输入中:
| 轮次 | 活跃请求 | 拼接的 input_ids | positions | query_start_loc | 新 token,按请求顺序 |
|---|---|---|---|---|---|
| 1 | A | [785,6722,315,9625,374] | [0,1,2,3,4] | [0,5] | [12095] |
| 2 | A、B | [12095,9707] | [5,0] | [0,1,2] | [13,21806] |
| 3 | A、B | [13,21806] | [6,1] | [0,1,2] | [576,0] |
| 4 | B | [0] | [2] | [0,1] | [358] |
第二轮的边界 [0,1,2] 表示:A 占拼接 Tensor 的 [0:1],B 占 [1:2]。positions=[5,0] 则表示 A 处理自己序列的位置 5,B 处理自己的位置 0。
边界是一组切分下标,不是 token 内容;每轮 batch 变化后会重新构造。请求 A 结束后,B 在第四轮成为唯一请求,所以它在拼接输入中的起点又回到了 0。
最终输出是:
A: [12095,13,576] → ' Paris. The'
B: [21806,0,358] → ' Answer! I'
这里的 ID 0 是实际 token,不能因为数值为零就认定它是 padding。两份输出仍属于各自请求,拼接计算没有把两段上下文混为一段。
批处理让多个请求共享一次执行过程,为提高设备利用率提供机会;这属于单设备上的请求批处理,不是把一个模型分布到多卡。此次实验验证了输入组织和请求隔离,没有测量吞吐提升,也没有记录足够的入队时序来解释为什么 B 没进入第一轮。
10. 序列变长以后:跨块,再归还块
前面的序列一直装得进一个 128 槽位的块。为观察跨块,我们构造了一个 127-token 输入:[785] * 126 + [374],再生成 3 个 token。它用于检查边界,不用于评价生成质量。
| 本轮 | 本轮位置 | 有效块表 | 写入槽位 |
|---|---|---|---|
| Prefill | 0~126 | [1] | 128~254 |
| Decode 1 | 127 | [1] | 255 |
| Decode 2 | 128 | [1,2] | 256 |
位置 127 仍属于逻辑块 0;位置 128 才进入逻辑块 1:
位置 127:block_table[0] × 128 + 127 = 255
位置 128:block_table[1] × 128 + 0 = 256
此次物理块恰好相邻,因此槽位也连续;实际分到不相邻的物理块同样可以工作。分页映射的作用,就是让序列的逻辑连续性不依赖物理块连续。
接下来,在独立的回收实验中连续运行两个相同请求。第一个请求的生命周期为:
| 时刻 | 请求持有块 | 空闲块数 | 请求状态 |
|---|---|---|---|
| 初始化 | 无 | 1256 | 尚未开始 |
| Prefill | [1] | 1255 | 运行中 |
| 第一次 Decode | [1] | 1255 | 运行中 |
| 第二次 Decode | [1,2] | 1254 | 输出达到长度上限 |
| 块释放返回后 | 已归还 | 1256 | 两块引用计数均归零 |
| 请求清理返回后 | 无 | 1256 | 已从调度器请求表移除 |
缓存池总共 1257 块,其中一个 null block 被保留,所以初始可用数是 1256。释放后的判断基准应是恢复到初始可用数,而不是所有块都空闲。
第二个请求分到 [2],跨块后变成 [2,1],结束后空闲数也回到 1256。这确认了先前归还的块可以再次分配;块号顺序不是固定承诺。
实际调度类为 Ascend BalanceScheduler,更新输出的方法来自 patch_kv_delivery_preemption.py。本次普通结束路径可概括为:
更新请求输出
→ check_stop 判断达到长度上限
→ 请求状态变为 FINISHED_LENGTH_CAPPED
→ 请求清理与 KVCacheManager.free
→ 释放请求的块映射
→ block_pool.free_blocks 减少引用计数
→ 可释放块进入空闲队列
归还块意味着改变缓存池的管理状态,不意味着把整块数据清零,也不意味着把预分配的 NPU 显存还给系统。 因此请求结束后,设备显存占用不一定明显下降。本次验证的是同步、关闭 Prefix Cache 的普通结束路径;取消请求、在途计算或缓存传输参与时的延迟释放不在验证范围内。
11. 带着实验结果回到源码
下表按本文的数据流排列阅读入口。路径分别相对于 vLLM 或 vLLM-Ascend 仓库根目录;它们对应实验镜像中的代码,其他版本可能调整类名、路径和调用关系。
| 想确认的问题 | 仓库及源码入口 |
|---|---|
| 调度哪些 token、怎样接收执行结果? | vLLM:vllm/v1/core/sched/scheduler.py;同时检查 Ascend 调度补丁 |
| 怎样准备输入并调用模型? | Ascend:vllm_ascend/worker/model_runner_v1.py,关注 _prepare_inputs、execute_model 和采样路径 |
| 模型主干和 logits 怎样计算? | vLLM:vllm/model_executor/models/qwen3.py |
| 本次 Qwen3 Attention 的实际 forward? | Ascend:vllm_ascend/patch/worker/patch_qwen3vl.py |
| K/V 怎样写入和参与 Attention? | Ascend:vllm_ascend/attention/attention_v1.py |
| 怎样选择下一个 token? | vLLM:vllm/v1/sample/sampler.py;Ascend:vllm_ascend/sample/sampler.py |
| 位置怎样转换成槽位? | Ascend:vllm_ascend/ops/triton/compute_slot_mapping.py |
| 请求的块怎样归还? | vLLM:vllm/v1/core/kv_cache_manager.py、single_type_kv_cache_manager.py、block_pool.py |
阅读时可以拿 Decode 1 自查:输入为什么是 [12095],位置为什么是 [5],选 hidden state 为什么用 [0],KV 槽位为什么是 [133],最后又为什么得到 13。能够把这五个值连到不同阶段,就已经把请求的一轮推进过程串起来了。
附录:证据范围与复现说明
这些观察完成于 2026-09-27,包含多次独立实验,并非一份运行日志。主线使用单请求同步执行;双请求、跨块和回收实验在对应章节明确改变了输入或配置。
观察点位于 Runner 输入准备之后、模型与第一层 Attention 的调用边界、logits 生成之后、采样返回处,以及调度器/缓存管理器的释放路径。部分观察使用临时源码插桩并在实验后恢复,回收实验使用进程内方法包装。提取 Tensor 会引入设备同步,因此本文日志不能用来评估性能。
原始实验记录存放在实验环境的 /workspace/learning/ 下:
| 实验 | 记录目录 |
|---|---|
| 单请求三轮、五轮 | trace_20260927/、trace5_20260927/ |
| 双请求 | trace2req_20260927/ |
| hidden states 与采样 | logits_20260927/ |
| 第一层 Q/K/V | layer0_20260927/ |
| KV 写入与局部 Attention 对照 | kvcheck_20260927/ |
| 增量计算与完整重算 | recompute_20260927/ |
| 跨块与请求结束回收 | crossblock_20260927/、freeblocks_20260927/ |
这些是作者实验环境中的目录索引,尚不是公开下载材料。当前记录保留了软件包版本,但没有完整固定模型 revision 和远程源码 Git commit,因此还不足以构成可以逐位复现的公开实验包。对外复现时,应补齐版本、模型来源及观察脚本。
本文结论限于记录中的模型、配置和测试输入:没有测试长上下文数值一致性、Prefix Cache 命中、异步释放、投机解码或多卡通信,也没有给出性能收益结论。最初启动测试中的退出告警没有在本文中完成根因定位;后续实验正常退出不能替代对该告警的解释。
更多推荐




所有评论(0)